feat(clmm): manage_clmm tool to recover orphaned LP positions; bin_count on get_pool_info - #204
feat(clmm): manage_clmm tool to recover orphaned LP positions; bin_count on get_pool_info#204fengtality wants to merge 67 commits into
Conversation
Greptile SummaryThe PR adds direct CLMM position management and orphaned-executor recovery, wires configurable liquidity-bin retrieval into pool information, and incorporates broader DEX and agent updates.
Confidence Score: 5/5The PR appears safe to merge because the previously reported recovery-routing failure is fixed and no blocking failure remains. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| mcp_servers/hummingbot_api/schemas.py | Adds CLMM request models and correctly routes resolve_orphan independently of whether an executor ID was supplied. |
| mcp_servers/hummingbot_api/tools/executors.py | Adds orphan listing and resolution flows, including an explicit required-input error for missing executor IDs. |
| mcp_servers/hummingbot_api/tools/gateway_clmm.py | Implements the direct CLMM management interface used to recover orphaned positions. |
| condor/agents/providers/executors.py | Exposes terminal executors retaining on-chain positions and provides concrete recovery instructions. |
| condor/runtime/danger.py | Classifies liquidity-mutating CLMM and AMM operations for confirmation while failing closed on malformed calls. |
| tests/test_executor_resolve_orphan.py | Verifies that missing-ID recovery requests reach the recovery handler and return the intended error. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Terminal LP executor] --> B{Position remains on-chain?}
B -- No --> C[Normal terminal state]
B -- Yes --> D[List via manage_executors orphaned]
D --> E[Close position via manage_clmm]
E --> F[resolve_orphan with executor_id]
F --> G[Remove recovery warning]
Reviews (39): Last reviewed commit: "docs(mcp): a raw token address is a vali..." | Re-trigger Greptile
… error Greptile P1 on #204: get_flow_stage() required executor_id for the resolve_orphan action, so a call missing the id silently fell through to show_schema/list_types and the recovery request was ignored. The action now always routes to resolve_orphan and the tool returns an explicit error pointing at action="orphaned" to find candidates. Also documents in the LP executor guide that resolve_orphan updates the API database only — an lp_rebalancer controller's in-memory orphan halt clears on controller restart, so restart after resolving. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
Local hummingbot-api-client v1.5.8 integration ✅
Test still in progress |
|
Test update:
Test orphan recovery acknolwedgement
Telegram stop handling for an already-terminated orphan
|
… error Greptile P1 on #204: get_flow_stage() required executor_id for the resolve_orphan action, so a call missing the id silently fell through to show_schema/list_types and the recovery request was ignored. The action now always routes to resolve_orphan and the tool returns an explicit error pointing at action="orphaned" to find candidates. Also documents in the LP executor guide that resolve_orphan updates the API database only — an lp_rebalancer controller's in-memory orphan halt clears on controller restart, so restart after resolving. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
add49f5 to
02c8d94
Compare
…t warnings, orphaned/resolve_orphan actions Condor-side of the gateway#678 retry-ownership work (canonical design: docs/retry-architecture.md in the companion gateway PR). Agents drive executors through manage_executors, where no controller exists to react to a stranded position — and the tick-prompt summary was RUNNING-only, so a terminal executor holding a live on-chain position was invisible: - ExecutorsProvider surfaces terminal executors that still own a position (involuntary POSITION_HOLD with hold_reason, the injected orphaned_position flag, or legacy FAILED-with-position) as a reason-aware ORPHANED POSITION warning with explicit recovery guidance: close via the gateway tools by position address — a fresh lp_executor CANNOT adopt an existing position and would mint a second one — then mark recovered. Also exposed as orphaned_executors in provider data. - manage_executors gains orphaned (list recovery candidates) and resolve_orphan (mark recovered) actions wired to the new API endpoints. - stop handler surfaces the already_terminated payload (close_type, position_address, orphan warning) instead of treating it as a plain stop; Telegram stop menu shows the same. - guides/lp_executor.md: close-exhaustion now terminates as an involuntary POSITION_HOLD (hold_reason=close_retries_exhausted), FAILED means nothing-left-on-chain, and the orphan recovery flow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HahKfEY9rvKnZijrzUAFSq
… error Greptile P1 on #204: get_flow_stage() required executor_id for the resolve_orphan action, so a call missing the id silently fell through to show_schema/list_types and the recovery request was ignored. The action now always routes to resolve_orphan and the tool returns an explicit error pointing at action="orphaned" to find candidates. Also documents in the LP executor guide that resolve_orphan updates the API database only — an lp_rebalancer controller's in-memory orphan halt clears on controller restart, so restart after resolving. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Regression cover for d8012bd: a resolve_orphan call with no executor_id must route to the resolve_orphan stage and return an actionable error, rather than falling through get_flow_stage() to show_schema/list_types and answering a recovery request with executor-type listings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Agents can now request the per-tick liquidity distribution around the active price via manage_gateway_clmm(action="get_pool_info", bin_count=N), which the companion API forwards to Gateway. Meteora always returns its bins; orca, raydium, uniswap and pancakeswap compute them on request, so the default of 0 keeps pool-info cheap. The client library's get_pool_info has no bin_count parameter, so requests with bins go straight to the endpoint — the same passthrough the executors tools use for newer routes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1.5.8 adds bin_count to gateway_clmm.get_pool_info, so the tool calls the typed client method instead of reaching past it to the endpoint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The orphan warning told the agent to "close the position via the gateway tools (remove liquidity by position address)" — an instruction with no implementing tool. Following it, an agent found that manage_executors(action="stop") is a no-op (the executor has already terminated, which is the correct contract), manage_amm handles AMMs only, and explore_dex_pools is read-only. The position stayed open and the recovery loop dead-ended. Adds manage_clmm, mirroring manage_amm: progressive-disclosure guide, per-action validation, dispatch to client.gateway_clmm.*. Actions: position_info, open, add_liquidity, remove_liquidity, close, collect_fees. Pool discovery stays in explore_dex_pools rather than being duplicated. Two details decide whether a recovery call actually works, and both are covered by tests: - an orphan records its DEX as lp_provider "orca/clmm" while Gateway routes on the bare "orca", so the connector is normalised - an lp_executor position is not in the API database, so close must forward pool_address or the API returns 400 The orphan listing now emits the concrete call rather than prose, built from the record's own fields — including that connector_name holds the network, not the DEX, which is the easiest way to construct a dead call by hand. Records still awaiting reconciliation emit no call, so no close is ever suggested with an unknown position address. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj
Four places told the agent (or the Telegram user) to close an orphaned position "via the gateway tools" — the tick-prompt warning, the Telegram stop menu, the manage_executors docstring, and the orphan listing. None named a tool that could do it, and the nearest guess, stopping the executor, is a no-op because it has already terminated. All four now name manage_clmm(action="close") and say plainly that stopping will not close the position. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj
pyproject already required ==1.5.8, but uv.lock still resolved 1.5.6, so every
`uv run --frozen` silently re-synced the venv back down. That downgrade is not
cosmetic: 1.5.6 has no pool_address on close_position and no add/remove
liquidity, so manage_clmm's close died with
TypeError: close_position() got an unexpected keyword argument 'pool_address'
The lock now names 1.5.8. Its sdist/wheel hashes are absent because 1.5.8 is not
published yet — they get filled in by a plain `uv lock` once hummingbot-api-client#25
merges and releases. `uv lock --check` accepts the entry as consistent with
pyproject in the meantime.
Until that release, `uv run --frozen` fails loudly:
Distribution `hummingbot-api-client==1.5.8` can't be installed because it
doesn't have a source distribution or wheel for the current platform
which is the honest state of the dependency, and better than silently running
against a client that cannot satisfy the tool. To work locally before the
release, install the client from source and skip the re-sync:
uv pip install --python .venv/bin/python ../hummingbot-api-client
uv run --no-sync python -m pytest tests/
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj
Rebasing onto #203 moves this branch onto a base that enforces `black --check .` and `isort --check .` in CI, which main did not. The code was written to match the surrounding pre-reformat style, so it needs a pass with the locked tools (black 26.1.0, isort 8.0.1). Formatting only — no behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj
Last agent still on `claude-acp:opus`; every other claude-acp agent already runs sonnet, so this makes the default uniform across the agent set. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj
…the executor's Same skill, two different config shapes, and carrying the executor's table into the controller silently mis-sizes a position. Verified against lp_rebalancer.py's _calculate_amounts and _calculate_price_bounds: - total_amount_quote is the WHOLE position in quote units — the controller splits it — where the executor takes base_amount and quote_amount separately. - position_width_pct is FULL width, so 5 means ±2.5%, while the w in the Bounds section above it is a half-width. The default 0.5 is ±0.25%. - position_offset_pct is ignored entirely on side=3 RANGE, which is always centred on P. On BUY/SELL its SIGN selects the mode: ≥0 single-sided out-of-range, <0 in-range needing both tokens. Plus the sizing trap: base_amt is recomputed from live P at open, so sizing total_amount_quote to exactly 2·B·P means any dip in P pushes the base requirement above the holding and the open fails when autoswap is off. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
…covers when_to_consult described the executor workflow only, so a request to deploy lp_rebalancer did not obviously route here — which is how a controller config gets built without first checking the parameter meanings that the lp_range_config skill spells out, and where total_amount_quote and position_width_pct differ from the executor's fields. Now names both lp_rebalancer and lp_executor, and says to consult for parameters BEFORE building config rather than after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
A `.env.bak-telegram` written by a config switcher carried a live OpenRouter API key into a commit. `.gitignore` had `.env`, which does not match it, and `git add -A` took it. GitHub push protection is what caught it — the key never reached the remote, and the commit has been rewritten to drop the file. `.env.*` and `*.bak-telegram` close the gap. Same shape as the bots/archived miss in hummingbot-api: a pattern naming one exact path while the thing it guards against arrives under a neighbouring name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
|
The four companion PRs have been reopened against current branches; this one stays as-is and is referenced from each:
(The previous set — gateway#679, hummingbot#8424, hummingbot-api#217, hummingbot-api-client#25 — is closed.) |
Two failures, both of which predate this branch's latest push.
**Tests died at collection.** `scripts_lp_test/` is named `test_*.py`, so pytest
collects it — but those files are live mainnet scripts that connect to a running
Hummingbot API at import time. With no API in CI:
ERROR scripts_lp_test/test_lp_read.py
ToolError: Failed to connect to Hummingbot API at http://localhost:8000
!!!! Interrupted: 1 error during collection !!!!
One import error stops the whole run, so none of the 2481 real tests executed.
The scripts are removed rather than excluded: they belong to a machine with
funded wallets and a running stack, not to a pull request. Collection now finds
2481 tests and they pass.
**Formatting.** `black --check .` failed on 17 files. Ran black and isort as CI
runs them; 503 files now left unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
Retargets this PR onto ralph's branch, which is where the agents/ churn (backpack_mm, brigado, the gate skills) actually originates. With that branch as the base, this PR's diff stops claiming those deletions as its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every agents/ change this branch carried on top of ralph's branch is lifted out and re-applied on docs/lp-rebalancer-controller-guidance, which is based on this one. agents/ here is now byte-identical to feat/ralph_improvemnts, so this PR's diff no longer claims the backpack_mm and brigado deletions — those are cardosofede's in 1cbc5dc and land with PR #214. Four of the seven commits involved touched agents/ and other paths at once, so the split is by path rather than by commit; the original commits stay in this branch's history and net to zero here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dry-run branch re-derived what the gate above it had already decided. Everything inside it is past `is_dangerous_tool_call`, which matched the call against DANGEROUS_*_ACTIONS per tool — so a second `action in ...` per tool was redundant, and the redundancy reopened SEC-093. That gate fails CLOSED on an unreadable action: `manage_clmm` with a missing, null or non-string `action` is dangerous by design. The re-check then read it as `""`, matched no set, fell through every branch, and reached the auto-approve tail — so in dry-run mode, the one mode whose whole promise is that nothing executes, a malformed CLMM, AMM or bot write executed for real. Same for manage_amm and manage_bots. Reaching that line is the decision; blocking is now unconditional. This also drops the DANGEROUS_AMM_ACTIONS and DANGEROUS_CLMM_ACTIONS imports, which existed only to feed the redundant checks. DANGEROUS_BOT_ACTIONS stays — the ownership check further down still needs it to tell a write from a read. The three sets stay separate in danger.py. They are what the outer gate needs to be per-tool, since each tool carves out different read-only actions, and test_dangerous_gate_names_resolve asserts each name against its own tool's literals — a merged set could only be checked against the union, which is weaker. Also drops test_confidence_is_graded_against_the_emitted_direction, the one test in tests/ that imports from agents/; it moves with its subject to the follow-up PR. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
usdm_premium_monitor and usdm_report are local instrument-specific routines that rode along on this branch; 2,111 lines with nothing in the tree importing them (routines are discovered from the folder, not imported by name). They are not part of the CLMM/LP work this PR is for. Recoverable from this branch's history if wanted: b6448ac and 4362d6a. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… not the executor's Belongs with the CLMM/LP work under test here, not with the hackathon sample agents — this documents behaviour of the stack this PR changes. An agent asked to run lp_rebalancer read lp_range_config's amounts table, which is written for lp_executor's separate base_amount/quote_amount, and carried that model into the controller, which takes a single total_amount_quote and splits it itself. It deployed a bot sized at half the intended base. Three ways the controller's fields differ, all verified against _calculate_amounts and _calculate_price_bounds: - total_amount_quote is the whole position. On side=3 RANGE the split is a hard 50/50 (quote = total/2, base = (total/2)/P), so deploying a base holding B needs 2*B*P; sizing it at B*P deploys half. - position_width_pct is a FULL width -- 5 gives +/-2.5%, where this skill's w is a half-width. - position_offset_pct is ignored entirely on RANGE, touching neither the amounts nor the bounds. A small negative offset 'to stay in range' is a no-op; the sign only selects single-sided vs in-range on BUY/SELL. AGENT.md names the controllers the consult routing covers: the same incident skipped consult entirely and went straight to raw tools. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ining it The list had drifted both ways, and both ways are silent. reload_handlers only touches names already in sys.modules, so a stale entry is a no-op and a missing one means the watcher logs '✅ Auto-reloaded handlers successfully' while the running bot goes on executing the old code — worse than no hot-reload, because it reports success. It named three handlers.dex.swap_* modules that do not exist, and omitted six that do: router, swap, liquidity, lp_monitor_handlers, geckoterminal and visualizations. Coverage under handlers/ was 37 of 75. Naming every module is what makes it work, because importlib.reload is NOT recursive. Reloading handlers.dex re-executes its __init__, but the 'from .router import ...' there re-binds the cached handlers.dex.router, so an edit to that file is missed unless the module is reloaded in its own right. For the same reason the derived list sorts children before parents: by the time a package's __init__ runs again, the submodules it imports from are fresh. handlers/ is now walked; the four modules outside it that handlers import from stay explicit. routines/ is deliberately not walked — routines have their own mtime-aware discovery in routines.base.discover_routines(force_reload=True), which owns reimporting individual routine modules, so only that base module is listed. condor.runtime.* stays unreachable by construction: it holds live agent subprocess handles, and re-executing it orphans every running agent. Per-module logging drops to debug with a count at info; 75 lines per keystroke was noise. Verified end-to-end: appending a constant to handlers/dex/router.py and calling reload_handlers() now picks it up, where before that module was never reloaded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
index.html loaded /charting_library/charting_library.standalone.js, a manually vendored folder that is not in public/ and is not an npm package. The SPA catch-all served index.html in its place, so every page load parsed HTML as JavaScript and threw 'SyntaxError: Unexpected token <' in the console, and each build printed a warning about the tag not being bundlable. Nothing used it. No file under frontend/src references TradingView or charting_library — that tag was the only mention in the repo. All seven chart call sites use lightweight-charts, which bundles normally. Zero functional change, and the build proves it: the JS bundle hash is unchanged at index-xa8_RtKH.js. Verified in the browser — the SyntaxError is gone and the dashboard renders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ep it positive My earlier text called offset 'ignored on RANGE' and left it there, which buries the point. side only applies to the FIRST position: after that the controller picks the side itself and only ever picks BUY or SELL (top exit -> BUY, bottom -> SELL; _determine_side_from_price returns nothing else, and _validate_and_clamp_bounds can swap BUY<->SELL but never back to RANGE). RANGE happens exactly once. Since RANGE ignores the field and BUY/SELL do not, position_offset_pct has no effect on the position being configured and governs every position after it. Reading its description as advice for the initial deposit is what went wrong on a live bot: -0.1 was chosen to make the first position double-sided, had zero effect there, and then forced every rebalance to be double-sided too. A top-exit close returns 100% quote, so each re-open asked for base the wallet no longer had -- 29 failed opens, 64 SLIPPAGE_EXCEEDED and 54 INSUFFICIENT_BALANCE. Now states the rule (positive -> single-sided, in whatever token the close returned) with the arithmetic from that position: at P=0.3075 width 10, -0.1% straddles spot and needs 1.267 base + 38.61 quote, +0.1% sits below spot and needs 0 base + 39.00 quote. Also flags that the field's own description promises 'autoswap will convert |offset|%' — true only if autoswap is on, which is easy to leave off when both tokens are in the wallet at deposit and every close after returns one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Retargets this PR onto main now that feat/ralph_improvemnts is merged there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s PR Sent back to the follow-up PR, where the agents are about to be revised so that each occupies a distinct role — the lp_range_config and AGENT.md wording is part of that revision, not of the CLMM/LP work under test here. This PR's agents/ diff is empty again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…consult routing Moved here from #204: this belongs with the sample agents, which are about to be revised so each occupies a distinct role. lp_range_config previously documented only lp_executor's separate base_amount/quote_amount. An agent asked to run the lp_rebalancer CONTROLLER read that table and carried the two-amount model into a tool that takes a single total_amount_quote and splits it itself, deploying a bot sized at half the intended base. Three ways the controller differs, verified against _calculate_amounts and _calculate_price_bounds: - total_amount_quote is the whole position. On side=3 RANGE the split is a hard 50/50, so deploying a base holding B needs 2*B*P. - position_width_pct is a FULL width -- 5 gives +/-2.5%. - position_offset_pct is a REBALANCE setting. side applies only to the first position; after that the controller always picks BUY or SELL, and RANGE ignores the offset while BUY/SELL do not. So the field has no effect on the position being configured and governs every one after it, where a negative value forces a two-sided re-open the wallet cannot fund: a top-exit close returns 100% quote. Keep it positive. AGENT.md names the controllers the consult routing covers — the incident that prompted this skipped consult entirely and went straight to raw tools. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… fallback manage_gateway_swaps opened with 'This is the ONLY swap surface.' That sentence is true of Gateway's ROUTES — pool-scoped /trading/amm/*-swap was folded into the unified route — but an agent reads it as a claim about tool choice, and there is nothing to correct it: manage_executors' docstring never says the word swap, and the order_executor guide calls itself 'the standard way to place buy/sell orders' with no mention of DEX or Gateway. So an agent looking for 'swap A for B' finds one tool claiming exclusivity and no alternative. That is what happened on a live session: both legs of an LP entry were swapped through manage_gateway_swaps at a flat slippage_pct=1.0. order_executor with connector_name=<network> and execution_strategy=MARKET reaches the same Gateway route and would have added two things a one-shot call cannot: - the slippage ramp. A one-shot swap carries one fixed tolerance and never retries at a wider one; on a thin pool that is the shape that failed 64 times on the ANSEM rebalancer, with nothing to widen. - an executor record, so the fill is tagged with controller_id and reaches PnL attribution. A one-shot swap is written to swap history only, so an entry and the position it funds land in different ledgers. The docstring now leads with that preference, says when this tool is still the right call (a quote with no execution, swap history, a connector the executor does not route to), and scopes the 'unified' claim to routes. The order_executor guide now states plainly that it is how you swap on a DEX, and that the router comes from the network's swapProvider rather than the order. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both were implemented in tools/gateway.py, given request schemas in schemas.py,
given formatters in formatters/gateway.py, and imported into server.py as
_impl -- and then never wrapped in @mcp.tool(). Grepping the _impl names in
server.py returned 2: the two import lines, never called. So the tools existed
everywhere except the registry, and no agent could reach them.
That had a visible cost. middleware.py appends GATEWAY_LOG_HINT to every Gateway
error -- "Check gateway logs for more details: manage_gateway_container(action=
'get_logs')" -- naming a tool that was not registered. And an agent asked whether
Gateway knew a token had no way to look: it searched for manage_gateway_config,
found nothing, and concluded the tool did not exist. It was there the whole time.
Gating: manage_gateway_config joins DANGEROUS_TOOLS but is gated on
resource_type, not action, because what it edits matters and how it edits does
not. The "wallets" resource with "add" takes a PRIVATE KEY, and "delete" removes
a signing wallet, so those need a human. Tokens, pools, connectors and networks
are Gateway's own symbol/address mapping: deleting a token there moves no funds
and changes nothing on-chain, and gating it would put a person in front of a
config edit while the trades that edit enables stay ungated.
_has_dangerous_resource is the resource-typed twin of _has_dangerous_action and
fails closed identically (SEC-093) -- an unreadable or missing resource_type
counts as dangerous. format_tool_summary names the wallet case precisely, so a
confirmation prompt reads "Import a solana wallet into Gateway (private key)"
rather than a generic line.
Two tests pin it: every gated resource exists on the tool and the set is exactly
{wallets}, no other resource needs confirmation under any action, and an
unreadable resource_type fails closed.
Registered tools: 13 -> 15.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9c8b378 to
2e23b05
Compare
… calls
pyproject resolves hummingbot-api-client==1.5.8 from a PR branch until 1.5.8 ships on
PyPI. A branch moves; the lockfile pins a commit on it. It had drifted four commits
behind, and one of those four added execute_quote -- which
mcp_servers/hummingbot_api/tools/gateway_swap.py already calls:
result = await client.gateway_swap.execute_quote(...)
$ git show eb6ab50:hummingbot_api_client/routers/gateway_swap.py | grep execute_quote
(nothing)
So a uv sync from this lockfile produced an AttributeError on the first real
invocation. CI never saw it: every test hands the tools a stub client that defines
whatever it is asked for, so the stub in test_execute_quote_action.py answered for a
method the installed package did not have -- green suite, broken dependency.
Re-lock onto the branch head, and add tests/test_client_surface.py, which asserts the
INSTALLED client carries what condor calls rather than what a stub will agree to. It
fails on the old lock and passes on the new one. pyproject now says to re-lock after
every push to that branch, and records the merge order: client PR merged and 1.5.8
published first, then this.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
…tration needed An agent asked to swap a token it had not seen before decided it had to add the token to Gateway's registry first, and to guess its decimals to do so -- "the address ends in meta, that suggests pump.fun, which is usually 6". It was right by luck. The same heuristic on DOGE-1 would have written 6 where the real value is 9, corrupting the mapping for every later trade. None of it was necessary. Gateway resolves an unregistered mint directly: quoting WIF (EKpQGSJt..., absent from the 30-token registry) by raw address returns a normal quote, price 0.196013, impact 0.0315%, approximation false. The token list is a symbol/address convenience, not a prerequisite for trading. manage_gateway_swaps said "Symbols or token addresses", which is true but too quiet to displace the belief that registration comes first. It now says the address does not have to be registered, that decimals are read on-chain, and not to add a token as a prerequisite. order_executor's guide -- now the preferred swap path -- said nothing at all about addresses, so it says the same thing there.


Summary
Condor-side of the gateway#678 retry-ownership work, plus the CLMM
bin_countwiring, and it absorbs the Derive Options Trader agent rename from the now-closed #202 (see below). Canonical design:docs/retry-architecture.md(in the gateway PR). Agents drive executors throughmanage_executors, where no controller exists to react to a stranded position — and the tick-prompt summary was RUNNING-only, so a terminal executor still holding a live on-chain position was invisible to the agent.Orphaned positions
POSITION_HOLDwithhold_reason, the injectedorphaned_positionflag, or legacyFAILED-with-position) as a reason-aware 🚨 ORPHANED POSITION line with explicit recovery guidance — a freshlp_executorcannot adopt an existing position and would mint a second one. Also exposed asorphaned_executorsin provider data.manage_executors: neworphaned(list recovery candidates) andresolve_orphan(mark recovered) actions wired to the new API endpoints.resolve_orphanwithoutexecutor_idnow returns a required-input error pointing ataction="orphaned". It previously fell throughget_flow_stage()toshow_schema/list_types, so an agent's recovery request was silently answered with executor-type listings.already_terminatedpayload (close_type, position address, orphan warning) is surfaced in both the MCP tool result and the Telegram stop menu instead of being treated as a plain stop.guides/lp_executor.md: close-exhaustion terminates as an involuntaryPOSITION_HOLD(hold_reason: close_retries_exhausted);FAILEDmeans nothing-left-on-chain; orphan recovery flow documented, including that thelp_rebalancerhalt is in-memory and clears on controller restart — so restart is the acknowledgment step after resolving.manage_clmm— the tool that closes an orphanThe lifecycle above could flag an orphan but not clear it. Every warning said to close the position "via the gateway tools" — an instruction with no implementing tool. An agent following it found that
manage_executors(action="stop")is a no-op (the executor has already terminated, which is the correct contract),manage_ammhandles AMMs only, andexplore_dex_poolsis read-only. The position stayed open.manage_clmmtool, mirroringmanage_amm: progressive-disclosure guide, per-action validation, dispatch toclient.gateway_clmm.*. Actions:position_info,open,add_liquidity,remove_liquidity,close,collect_fees. Pool discovery stays inexplore_dex_poolsrather than being duplicated.lp_provider: "orca/clmm"while Gateway routes on the bare"orca", so the connector is normalised; and anlp_executorposition is absent from the API database, soclosemust forwardpool_addressor the API returns400.connector_nameholds the network, not the DEX, which is the easiest way to hand-build a dead call. Records awaiting reconciliation emit no call, so a close is never suggested with an unknown position address.manage_executorsdocstring, orphan listing) now namemanage_clmmand state that stopping will not close the position.CLMM
bin_countmanage_gateway_clmm(action="get_pool_info", bin_count=N)requests the per-tick liquidity distribution around the active price. Meteora always returns its bins; orca, raydium, uniswap and pancakeswap compute them on request, so the default of0keeps pool-info cheap. Requireshummingbot-api-client1.5.8 (pinned here) for the typed parameter.Absorbs #202 (Derive Options Trader)
#202 renamed the
smart_money_flowagent toderive_options_trader, added theoptions_flowroutine andoptions_oracle_operatorstrategy, and moved both strategies off the opencode custom endpoint ontoclaude-acp:sonnet. It was branched frommainbefore #203, so it missed two gates #203 adds — and its own CI stayed green because those gates don't exist on its base. Merging it here rather than after would have turnedmainred on merge.test_nobody_hand_builds_a_geckoterminal_url, which fails any source containingapi.geckoterminal.com. feat(agents): rename Smart-Money Flow agent to Derive Options Trader #202 carriedmain's pre-fixonchain_flow.py, which set its own base URL and opened a barehttpx.AsyncClient, bypassing the shared per-IP budget owned bycondor/pool_data.py. Simulating the merge reproduced the failure. No code change was needed in the end: feat(agents): rename Smart-Money Flow agent to Derive Options Trader #202 only renamed that file, and Feat/trade panel dex #203 had already rewritten it to callpool_data.gecko_request, so the merge kept the rename with the rate-gated body. Verified byte-identical tomain's copy first, so nothing of feat(agents): rename Smart-Money Flow agent to Derive Options Trader #202's was dropped.options_flow.pyfailedblack --checkandisort --check, which Feat/trade panel dex #203 enforces repo-wide. Reformatted.options_flow.py:433wrote a Markdown underscore escape as\_inside a non-raw string. Python does not recognise\_, so it kept both characters but raisedSyntaxWarning— an error in a future Python. Doubled to\\_; rendered output unchanged.#202 is closed with a comment pointing here.
Dependency: requires the unreleased client 1.5.8
pyproject.tomlrequired==1.5.8whileuv.lockstill resolved 1.5.6, so everyuv run --frozensilently re-synced the venv back down. That downgrade is not cosmetic — 1.5.6 has nopool_addressonclose_position, somanage_clmm's close died with:The tool's unit tests stub the client object, so they passed throughout; only a live call surfaced it. The lock now names 1.5.8. Its sdist/wheel hashes are absent because 1.5.8 is not on PyPI yet — a plain
uv lockfills them in once hummingbot/hummingbot-api-client#25 merges and releases, anduv lock --checkaccepts the entry as consistent withpyproject.tomlmeanwhile.Until that release
uv run --frozenfails loudly (can't be installed because it doesn't have a source distribution or wheel), which is the honest state of the dependency and strictly better than silently running against a client that cannot satisfy the tool. To work locally before the release:This PR merges last — after #203, then after hummingbot/hummingbot-api-client#25 is released.
Companion PRs
docs/retry-architecture.md(in the gateway PR)binCounton unified CLMM pool-infoPOSITION_HOLD, fresh position reads, bounded pending-tx pollingbin_countpassthrough, Raydium routed through Gatewaybin_countonget_pool_info(1.5.8)bin_countonget_pool_infoBased on #203
This PR targets
feat/trade-panel-dex(#203), notmain, since #203 merges first. It has been rebased onto that branch, so the diff here is only this branch's own commits (16 files) rather than 330.Conflicts resolved during the rebase:
tools/gateway_clmm.py— Feat/trade panel dex #203's plainget_pool_infocall vs. this branch'sbin_count. Resolved to the typedbin_count=call in Feat/trade panel dex #203's formatting; the intermediate_getpassthrough commit is superseded later in the series.formatters/__init__.py— Feat/trade panel dex #203 reformatted imports and moved the gateway block below executors;format_clmm_resultadded to the relocated block.schemas.py— Feat/trade panel dex #203 reflowed theAMMRequesttail to multi-line; kept their formatting and appendedCLMMRequestafter it.uv.lock— Feat/trade panel dex #203 bumps the client to the published 1.5.7, this branch needs 1.5.8 (see below); resolved to 1.5.8.#203 also adds a CI gate (
black --check .,isort --check .) thatmaindoes not have, so a formatting-only commit runs the locked tools (black 26.1.0, isort 8.0.1) over this branch's files.The liquidity depth column in #203 shows bins only for Meteora until
fetch_liquidity_binspassesbin_count— details and the one-line change are in a comment on that PR.Merge order
Until step 2,
uv synchere cannot install (hummingbot-api-client==1.5.8is not yet on PyPI), so CI will be red at the dependency step. Lint/test failures inherited from the base branch are being addressed in #203; all 12 Python files in this PR passblackandisortunder the locked versions.Validation
2268 tests pass on the rebased branch (
uv run --no-syncagainst a locally built 1.5.8), including 24 coveringmanage_clmmdispatch/validation and the exact recovery call the orphan listing emits, plus theresolve_orphanrequired-input contract. One unrelated test,test_agents.py::test_numeric_credentials_reach_the_subprocess_as_strings, fails identically on the barefeat/trade-panel-dexbase. Validated live on mainnet with the companion branches deployed: forced close-failure cascade → 🚨 orphan surfaced in the tick prompt →already_terminatedon re-stop → recovery →resolve_orphancleared it.bin_count=4returns populated bins for orca and raydium through the MCP tool;bin_count=0returns none.🤖 Generated with Claude Code
https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj