Skip to content

feat(clmm): manage_clmm tool to recover orphaned LP positions; bin_count on get_pool_info - #204

Open
fengtality wants to merge 67 commits into
mainfrom
feat/lp-close-retry-ownership
Open

feat(clmm): manage_clmm tool to recover orphaned LP positions; bin_count on get_pool_info#204
fengtality wants to merge 67 commits into
mainfrom
feat/lp-close-retry-ownership

Conversation

@fengtality

@fengtality fengtality commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Condor-side of the gateway#678 retry-ownership work, plus the CLMM bin_count wiring, 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 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 still holding a live on-chain position was invisible to the agent.

Orphaned positions

  • 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 line with explicit recovery guidance — a fresh lp_executor cannot adopt an existing position and would mint a second one. Also exposed as orphaned_executors in provider data.
  • manage_executors: new orphaned (list recovery candidates) and resolve_orphan (mark recovered) actions wired to the new API endpoints.
  • resolve_orphan without executor_id now returns a required-input error pointing at action="orphaned". It previously fell through get_flow_stage() to show_schema/list_types, so an agent's recovery request was silently answered with executor-type listings.
  • Stop handling: the already_terminated payload (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 involuntary POSITION_HOLD (hold_reason: close_retries_exhausted); FAILED means nothing-left-on-chain; orphan recovery flow documented, including that the lp_rebalancer halt is in-memory and clears on controller restart — so restart is the acknowledgment step after resolving.

manage_clmm — the tool that closes an orphan

The 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_amm handles AMMs only, and explore_dex_pools is read-only. The position stayed open.

  • New manage_clmm tool, 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 works, both test-pinned: an orphan records its DEX as lp_provider: "orca/clmm" while Gateway routes on the bare "orca", so the connector is normalised; and an lp_executor position is absent from the API database, so close must forward pool_address or the API returns 400.
  • The orphan listing now emits the concrete call, built from the record's own fields — including that connector_name holds 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.
  • All four dead-end warnings (tick prompt, Telegram stop menu, manage_executors docstring, orphan listing) now name manage_clmm and state that stopping will not close the position.

CLMM bin_count

manage_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 of 0 keeps pool-info cheap. Requires hummingbot-api-client 1.5.8 (pinned here) for the typed parameter.

Absorbs #202 (Derive Options Trader)

#202 renamed the smart_money_flow agent to derive_options_trader, added the options_flow routine and options_oracle_operator strategy, and moved both strategies off the opencode custom endpoint onto claude-acp:sonnet. It was branched from main before #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 turned main red on merge.

#202 is closed with a comment pointing here.

Dependency: requires the unreleased client 1.5.8

pyproject.toml required ==1.5.8 while 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, so manage_clmm's close died with:

TypeError: close_position() got an unexpected keyword argument 'pool_address'

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 lock fills them in once hummingbot/hummingbot-api-client#25 merges and releases, and uv lock --check accepts the entry as consistent with pyproject.toml meanwhile.

Until that release uv run --frozen fails 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:

uv pip install --python .venv/bin/python ../hummingbot-api-client
uv run --no-sync python -m pytest tests/

This PR merges last — after #203, then after hummingbot/hummingbot-api-client#25 is released.

Companion PRs

Based on #203

This PR targets feat/trade-panel-dex (#203), not main, 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.pyFeat/trade panel dex #203's plain get_pool_info call vs. this branch's bin_count. Resolved to the typed bin_count= call in Feat/trade panel dex #203's formatting; the intermediate _get passthrough commit is superseded later in the series.
  • formatters/__init__.pyFeat/trade panel dex #203 reformatted imports and moved the gateway block below executors; format_clmm_result added to the relocated block.
  • schemas.pyFeat/trade panel dex #203 reflowed the AMMRequest tail to multi-line; kept their formatting and appended CLMMRequest after it.
  • uv.lockFeat/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 .) that main does 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_bins passes bin_count — details and the one-line change are in a comment on that PR.

Merge order

  1. Feat/trade panel dex #203 — the base of this branch
  2. feat(clmm): bin_count, CLMM liquidity methods, and orphan-closable positions (1.5.8) hummingbot-api-client#25 — released to PyPI as 1.5.8
  3. this PR

Until step 2, uv sync here cannot install (hummingbot-api-client==1.5.8 is 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 pass black and isort under the locked versions.

Validation

2268 tests pass on the rebased branch (uv run --no-sync against a locally built 1.5.8), including 24 covering manage_clmm dispatch/validation and the exact recovery call the orphan listing emits, plus the resolve_orphan required-input contract. One unrelated test, test_agents.py::test_numeric_credentials_reach_the_subprocess_as_strings, fails identically on the bare feat/trade-panel-dex base. Validated live on mainnet with the companion branches deployed: forced close-failure cascade → 🚨 orphan surfaced in the tick prompt → already_terminated on re-stop → recovery → resolve_orphan cleared it. bin_count=4 returns populated bins for orca and raydium through the MCP tool; bin_count=0 returns none.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Mt84XBEMVxbbyMG8fDxDKj

@greptile-apps

greptile-apps Bot commented Aug 13, 2026

Copy link
Copy Markdown

Greptile Summary

The 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.

  • Surfaces orphaned LP positions and provides explicit close-and-resolve recovery guidance.
  • Adds manage_clmm actions for concentrated-liquidity position operations.
  • Adds bin_count support and updates the API-client dependency lock.
  • Corrects missing-ID routing for resolve_orphan with an explicit required-input response.

Confidence Score: 5/5

The PR appears safe to merge because the previously reported recovery-routing failure is fixed and no blocking failure remains.

No blocking failure remains.

Important Files Changed

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]
Loading

Reviews (39): Last reviewed commit: "docs(mcp): a raw token address is a vali..." | Re-trigger Greptile

Comment thread mcp_servers/hummingbot_api/schemas.py Outdated
fengtality added a commit that referenced this pull request Aug 13, 2026
… 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>
@greptile-apps

greptile-apps Bot commented Aug 13, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

@fengtality fengtality changed the title feat(executors): surface orphaned LP positions to agents — tick-prompt warnings, orphaned/resolve_orphan actions feat(clmm): surface orphaned LP positions to agents; bin_count on get_pool_info Aug 13, 2026
@rapcmia

rapcmia commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Test update:
image

  • Build local hbot-lib image using hbot/8424
  • Integrate the local hbot-lib to HAPI/217 and build local images including GW/679
  • Integrate HAPI-client/25 on local v1.5.8 and setup with condor/204
  • Deploy HAPI/217 make setup; make deploy successfully
  • Install and run condor/204 make install; make run ok
  • Successfully deploy webUI

Local hummingbot-api-client v1.5.8 integration ✅

  • Condor started with the local Hummingbot API client version 1.5.8 and connected successfully to the local Hummingbot service.
  • Read-only pool lookups worked for Orca, Meteora DAMM v2, and Meteora DLMM. The returned information included prices, fees, reserves, and liquidity details.
  • The Meteora DAMM v2 pool-info tool returned the expected price, reserves, and fee data.
  • A Meteora DLMM lookup accepted bin_count=20 and returned 141 liquidity-bin entries around the active price.
  • Meteora returns its available bins, so this is expected and is not limited to 20 entries.

Test still in progress

@rapcmia

rapcmia commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Test update:

  • Condor used the local API client successfully and read Orca and Meteora pool information. ✅
  • Empty orphan checks, missing recovery selection, and repeat stops on ended executors behaved safely. ✅
  • A real Orca position that could not close was clearly shown as an orphaned position instead of disappearing. ✅
  • Condor showed the orphan warning and affected position ✅
    #### Test scenario
      - Opened LP excutor on Orca
      - Transferred all SOL balance to USDC until it is flat 0
      - Asked agent to close the position
      - Retries until 10/10 occurred 
    
    #### live orphan state is now confirmed.
    curl -sS -u admin1:admin2 -H 'Content-Type: application/json' \
      -X POST http://localhost:8000/executors/search \
      -d '{"executor_types":["lp_executor"]}' | jq
    
    {
      "executor_id": "36Xg…bcMn",
      "status": "TERMINATED",
      "close_type": "POSITION_HOLD",
      "position_address": "8i1u…sUhA",
      "current_retries": 11,
      "max_retries_reached": true,
      "hold_reason": "close_retries_exhausted"
    }
    
    curl -sS -u admin1:admin2 \
      http://localhost:8000/executors/positions/orphaned | jq
    
    {
      "count": 1,
      "orphans": [{
        "executor_id": "36Xg…bcMn",
        "close_type": "POSITION_HOLD",
        "position_address": "8i1u…sUhA",
        "hold_reason": "close_retries_exhausted"
      }]
    }
    
  • The close process retried with increasing delays before stopping and leaving the position visible. ✅
    • It waited longer between attempts: 2, 4, 8, 16, then 30 seconds.
    • After the normal limit of 10 retries, the next failed attempt stopped the
      executor but kept the open position visible for recovery.
  • Marking an orphan as recovered removed it from the warning and recovery list. ✅
    • From the test i did (resolved_orphan), it seems it was a acknowledgement and did not close the position (using tools or gateway route) using Orca and meteora ❗

Test CLMM bin_count

image
  • Orca and Raydium pool lookups both returned no bins with bin_count=0 and exactly 20 bins with bin_count=20.
  • Meteora accepted the value but returned all 141 populated bins for both requests. This is its expected connector behavior, it does not limit bins.
  • All checks used the normal read-only pool lookup. No positions, trades, or funds were changed.

@rapcmia

rapcmia commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Test update:

  • Setup a orphaned lp_executor
    curl -sS --max-time 15 -u "$HBOT_API_USER:$HBOT_API_PASSWORD" \
      http://localhost:8000/executors/positions/orphaned | jq
    
    {
      "count": 1,
      "orphans": [
        {
          "executor_id": "HJJUFaSZdThCH6rRv5agcVCZ4widziThm1aRdh2LtcTW",
          "executor_type": "lp_executor",
          "connector_name": "solana-mainnet-beta",
          "trading_pair": "SOL-USDC",
          "close_type": "POSITION_HOLD",
          "position_address": "H4vD69DsraHjHyKvRwRPHVGe2aJkvAUaNK5tMif2CiNw",
          "hold_reason": "close_retries_exhausted",
          "needs_onchain_reconciliation": false
        }
      ]
    }
    

Test orphan recovery acknolwedgement

  • The orphan warning tells the agent to close the position using gateway tools, then call resolve_orphan.
  • For Meteora DLMM or Orca Whirlpool LP-executor positions, the currently exposed MCP tools cannot close the on-chain CLMM position:
    • manage_executors(action="stop") is a no-op because the executor is already terminated.
    • manage_amm(action="remove_liquidity") does not support these CLMM position types. Its Meteora support is for DAMM v2; Orca is unsupported.
    • explore_dex_pools is read-only.
  • The underlying Gateway API has a CLMM close endpoint, but no MCP tool currently exposes it.

Telegram stop handling for an already-terminated orphan

Stop executor HJJUFaSZdThCH6rRv5agcVCZ4widziThm1aRdh2LtcTW.


✅ mcp.condor.consult (19s)

I’ll submit the stop request only for that executor.Stop request completed as a no-op: the executor was already TERMINATED with POSITION_HOLD.

No LP position or orphan record was changed.
  • It accepted the stop request.
  • It correctly treated it as a no-op.
  • The executor remained terminated.
  • No LP position or orphan record was changed.

@fengtality fengtality changed the title feat(clmm): surface orphaned LP positions to agents; bin_count on get_pool_info feat(clmm): manage_clmm tool to recover orphaned LP positions; bin_count on get_pool_info Aug 17, 2026
fengtality added a commit that referenced this pull request Aug 17, 2026
… 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>
@fengtality
fengtality force-pushed the feat/lp-close-retry-ownership branch from add49f5 to 02c8d94 Compare August 17, 2026 17:54
@fengtality
fengtality changed the base branch from main to feat/trade-panel-dex August 17, 2026 17:55
@fengtality

Copy link
Copy Markdown
Contributor Author

@greptile-apps

@fengtality
fengtality changed the base branch from feat/trade-panel-dex to main August 18, 2026 18:35
fengtality and others added 10 commits August 18, 2026 11:36
…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
@rapcmia rapcmia moved this from Backlog to Under Review in Pull Request Board Aug 21, 2026
fengtality and others added 3 commits August 20, 2026 21:25
…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
@fengtality

Copy link
Copy Markdown
Contributor Author

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.)

fengtality and others added 3 commits August 20, 2026 22:17
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>
@fengtality
fengtality changed the base branch from main to feat/ralph_improvemnts August 21, 2026 14:27
fengtality and others added 3 commits August 21, 2026 07:44
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>
fengtality and others added 4 commits August 21, 2026 08:32
…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>
@fengtality
fengtality changed the base branch from feat/ralph_improvemnts to main August 21, 2026 16:57
…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>
fengtality added a commit that referenced this pull request Aug 21, 2026
…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>
fengtality and others added 2 commits August 21, 2026 11:27
… 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>
@fengtality
fengtality force-pushed the feat/lp-close-retry-ownership branch from 9c8b378 to 2e23b05 Compare August 21, 2026 21:51
fengtality and others added 2 commits August 21, 2026 15:12
… 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Under Review

Development

Successfully merging this pull request may close these issues.

2 participants