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 107 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 107 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

This PR adds end-to-end visibility and recovery tooling for orphaned CLMM positions, wires configurable liquidity-bin retrieval into CLMM pool information, and incorporates agent and strategy updates.

  • Adds manage_clmm actions for position inspection and liquidity lifecycle operations.
  • Adds orphan listing, recovery guidance, and resolve_orphan support to executor tooling.
  • Surfaces already-terminated executors that still hold on-chain positions.
  • Passes bin_count through CLMM pool-info requests and updates the API client dependency.
  • Renames and extends the Derive Options Trader agent while adding adaptive-grid and market-analysis content.

Confidence Score: 5/5

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

No blocking failure remains.

Important Files Changed

Filename Overview
mcp_servers/hummingbot_api/schemas.py Extends executor and CLMM request schemas; resolve_orphan now routes 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 response when executor_id is absent.
mcp_servers/hummingbot_api/tools/gateway_clmm.py Implements validated CLMM position and liquidity-management dispatch through the API client.
condor/agents/providers/executors.py Detects terminal executors retaining live positions and emits actionable orphan-recovery information.
mcp_servers/hummingbot_api/formatters/gateway.py Adds formatting support for CLMM operation results.
pyproject.toml Pins the API client version required by the new CLMM parameters and close-position contract.
uv.lock Aligns the locked API client version with the new runtime requirement.

Sequence Diagram

sequenceDiagram
    participant Agent
    participant Executors as manage_executors
    participant CLMM as manage_clmm
    participant Gateway
    Agent->>Executors: orphaned
    Executors-->>Agent: executor and position recovery details
    Agent->>CLMM: close(position_address, pool_address)
    CLMM->>Gateway: close_position
    Gateway-->>CLMM: position closed
    CLMM-->>Agent: close result
    Agent->>Executors: resolve_orphan(executor_id)
    Executors-->>Agent: orphan marked recovered
Loading

Reviews (27): Last reviewed commit: "docs(gateway): record GW-25, narrow in-r..." | 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
fengtality and others added 19 commits August 20, 2026 06:55
Every monetary field is Type.Number({format:'decimal'}) — 181 of them — which is an IEEE
754 double on the wire, so exact on-chain decimals do not round-trip: fees came back as
4.2900000000000047E-7 and 0.00003700000000250725, a swap recorded input_amount
0.010000000000000002. The format annotation is a hint no parser acts on.

It also defeats hummingbot-api's Decimal models, which are exact but receive a value that
already lost precision. And one instance was not cosmetic: page=2 became the float 2.0 and
went out on a query string as "2.0", fixed with _wire_str at the client edge.

Cosmetic at the amounts tested, but the error is relative — 18-decimal tokens and
sums across events are where it stops being.

Also renders the withdrawn liquidity and the rent refund on a CLMM close, which showed
only fees. The three are different kinds of money and a close looked like it recovered
almost nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
A CLMM range has three possible relationships to spot and the position holds something
different in each: entirely above it is 100% base, entirely below 100% quote, across spot
both. The script only covered "across" (as `open`) and one hardcoded above-spot range, and
funded the above-spot case from a range that had been chosen when spot was elsewhere.

open-above / open-below / open-across now each fund only the side their range can use, and
derive the range from live pool-info. A written-down range is only above or below spot
until the market moves — spot drifted ~2% over the session that prompted this, which is
enough to turn a one-sided range into a straddling one and quietly stop testing what the
step is named for. Each range is 1% wide against the pool's bin_step of 4, so snapping
cannot collapse it, and the one-sided ones clear spot by 2%.

Also renders the withdrawn liquidity and rent refund on a CLMM close, which showed only
fees — a close that returned 0.0053 SOL of liquidity and 0.0101 SOL of rent reported as if
it had recovered a rounding error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
Opening one-sided ranges on both sides of spot showed each depositing exactly slippagePct
more than asked: 1 USDC funded became 1.009999, 0.01 SOL became 0.010099999. The two-sided
open in the same minute came in under on both sides, which is why this was never visible
before — with both sides funded the pool ratio pins the deposit and the bound is never
reached.

openPosition.ts:142 passes tokenMaxA/tokenMaxB — the slippage-inflated ceilings from the
quote — as the amounts to deposit, then passes slippageToleranceBps alongside them, so the
tolerance is applied twice and the ceiling becomes the target. tokenEst* is what the
deposit should be.

Records the design question underneath too: slippage protects a ratio, and a one-sided
position has none, so arguably no tolerance belongs in that amount at all. Meteora's
equivalent passes the caller's figure and keeps slippage as a separate tolerance; its
one-sided open deposited under, as it should.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
The file had grown to 1035 lines of equal-weight write-ups, so the seven things still
worth doing were buried among the eighteen that are done. Open issues keep their full
text and lead the document, worst first — only GW-18 is a wrong number; the rest are ways
a wrong request is accepted quietly. Fixed issues collapse to a line each: what it was,
what closed it, which commit. Nothing is renumbered.

The six outstanding `Verify` notes are collected into one section rather than dissolving
with the write-ups that carried them, since none has been re-run since it landed.

Two entries corrected while condensing. GW-18's stated fix — copy orca/closePosition.ts —
was investigated and does not work: extractInnerTransferAmounts groups by top-level
instruction, which separates Orca's two but not Raydium-style decrease_liquidity_v2, where
principal and fees move inside one. It needs a live pancakeswap-sol close, which is now
what the entry says. GW-20 records that its own hummingbot-api half added the rent columns
to the wrong model, taking the AMM position search down until 55cf09e.

Also corrects the record on 1529d04. That commit's message describes an Orca
slippage-bound finding; its contents are entirely the GW-13 and GW-24 write-ups from a
parallel session, swept in by a `git add -A` over a shared working tree. The message is
wrong and the contents are not. It is already pushed, and force-pushing a shared branch
with another session live on it is not worth a message fix, so it stands as written and is
corrected here. GW-25 remains free for the Orca finding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
Three Orca positions closed in the same pool minutes apart. Both out-of-range ones closed
first time at the default 1% tolerance; the in-range one, 1% wide and sitting 0.13% below
its upper bound, failed twice with TokenMinSubceeded (0x1782).

The two failures are at different stages and that is the part worth knowing: the first was
caught in simulation and cost nothing, the second landed on-chain and reverted, paying
0.000011772 SOL. Retrying is therefore not free once an attempt clears simulation.

Records the tolerance hypothesis explicitly as UNPROVEN. The third attempt used 5% and
succeeded, but the position had drifted out of range by then, so the experiment cannot
separate the two explanations — every close that has succeeded here was out of range, and
both failures were in range. The discriminating test is written down: a narrow, still
in-range position closed at high tolerance.

On retries: the executor's paced re-entry is implemented and correct for a stale quote —
backoff, fresh state, 10 attempts. The gap is that nothing widens tolerance;
lp_executor.py:938 omits slippage_pct so every attempt re-quotes just as tight as the last.
Recommends escalating slippage across the existing budget, capping it in config beside
max_retries, counting simulation failures separately from paid revert failures, and
treating out-of-range as a signal to stop widening.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0166iQoxKce23GkUwuQJxdkr
…h and the four producers

Everything Condor said when a background task finished went to Telegram and
nowhere else, so a user with only the dashboard open learned nothing.

condor/notifications.py is the other half: a per-user JSON store under data/
(atomic, capped, newest first, serialised behind a write lock) plus a push bus
that surfaces register themselves on, the same shape runtime/wake.py already
uses. chat_ws registers a sink emitting a `notification` event on the socket
every authenticated page already holds; GET/POST /api/v1/notifications is the
durable path for a notice raised while no tab was open.

The four producers -- a finished delegation, a finished routine, /agents/notify
and the boot notice -- record alongside their existing Telegram push, with the
same text, so the surfaces cannot tell different stories about one task. A
routine with no conversation behind it (the scheduler, the dashboard) now
reaches its owner for the first time.

NotifyBot is the last rung of resolve_bot(): with no TELEGRAM_TOKEN at all an
outbound message becomes a bell item instead of being dropped by a sender that
cannot deliver. It sits below both Telegram rungs, so a configured install is
unchanged and nothing is ever pushed to Telegram twice.

FEAT-048
A bell next to the Bug and Settings buttons, with an unread badge and a
dropdown of the last 50 notices; an item carrying a link navigates there, and
opening the list marks it read.

Two sources, one react-query key: history from GET /notifications on mount,
live arrivals written into the same cache by the chat socket's `notification`
handler. So a notice that arrived while the tab was open and one that arrived
an hour ago are indistinguishable, and a socket that was down costs nothing but
latency.

FEAT-048
Named operations and a published error envelope, symmetric response component names, and
the dead-code half of GW-16: 36 leftover schema exports, the 22 tests that asserted them,
and the three disagreeing parseChainNetwork implementations collapsed into one.

GW-16 stays partly open on purpose. The three addressing conventions and the 20 route
files that still switch on connector for every liquidity operation are refactors of live
trading paths, not cleanups, and want their own change rather than riding along with a
dead-code sweep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
Two sessions edit this file at once, so any `git add -A` swept whichever one was mid-edit
into the other's commit. That is how 1529d04 came to carry a message describing an Orca
slippage finding while containing, entirely, a parallel session's GW-13 and GW-24
write-ups — and it nearly happened twice more since.

The file stays on disk and keeps its history; it is only out of the index, so neither
session can commit the other's half-written paragraph again.

Anyone who pulls this loses their working copy, since removing a tracked file is a deletion
like any other. `git show HEAD~1:GATEWAY_ISSUES.md > GATEWAY_ISSUES.md` brings it back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
… and /auth/local-login

Telegram was both the runtime dependency and the identity provider: main()
could not build an Application without a token, and the dashboard's only way
to get a session was the one-time token /web mints. CONDOR_MODE=local now
boots the process with no bot at all and logs the local admin straight in.

The mode is explicit and is never inferred from whether a token happens to be
present — telegram mode with an empty TELEGRAM_TOKEN exits at boot naming
`make setup`, so an install that loses its token stops instead of quietly
becoming a login-less dashboard. Local mode binds 127.0.0.1 only, with
WEB_HOST as the one documented opt-out. Those two controls are independent
and both required; tests pin each of them.

Boot skips the PTB lifecycle rather than stubbing it: the Application is built
on a placeholder token but never initialized, so nothing polls and no handler
can dispatch, and the job queue is started directly (scheduled routines,
update checks and signals all keep working). Outbound messages resolve to
NotifyBot, so a routine's context.bot.send_message lands on the dashboard bell.

Auth adds one endpoint. get_current_user, the WS handshake, check_server_access
and the 200-odd guarded endpoints are untouched — /auth/local-login mints an
ordinary JWT and 404s outside local mode, so nothing downstream ever learns
which mode it is in.
Setup opens with the choice — Telegram (today's prompts, verbatim, plus an
explicit CONDOR_MODE=telegram) or Local, which asks nothing: it writes
CONDOR_MODE=local, ADMIN_USER_ID=1 and the localhost WEB_URL, and prints what
local mode costs. An install from before this existed has a token but no
CONDOR_MODE; the next run records telegram for it, so the mode is always
explicit on disk.

The Login page asks the server how this install authenticates before it
renders, so the "run /web in Telegram" card never flashes on a machine that
has no Telegram, and local mode lands straight on the dashboard.

README documents local mode as what it is: no login, loopback only, and
WEB_HOST=0.0.0.0 as a deliberate act with a warning attached.
The AMM and CLMM write formatters printed a position, a transaction and a status,
and nothing about the money. A close that moved 3188 DOGE-1 and 0.0219 SOL reported
neither, though hummingbot-api returns both — they reach its database, which is how
the 4x rent inflation in that very close was found. The CLMM close branch twenty
lines away renders liquidity, fees and rent properly; these were the odd ones out.

The two surfaces answer in different shapes and each branch now reads its own:
/clmm/add and /clmm/remove return a flat snake_case dict keyed `transaction_hash`,
while the AMM writes wrap Gateway's camelCase `data` inside an AMMTransactionResponse
keyed `signature`. Rent is printed as its own line where it exists, because it is
neither income nor principal — and omitted where it does not, since a partial removal
closes no account and refunds nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
Three new config fields — slippage_pct, slippage_multiplier, max_slippage_pct — and
what the defaults do: 0.05 → 0.25 → 1.25 → 5, widening only on a slippage failure,
resetting at each phase boundary, and at the ceiling an exit keeps trying while an
entry stops. Also which connectors actually enforce a minimum on the close, since on
three of them the ramp changes nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
The last of the three layers that left Gateway's two-step flow unreachable. quote gives
a quote_id on the router connectors that hold a price; execute_quote commits to that
quote instead of re-pricing at execution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
…t/lp-close-retry-ownership

# Conflicts:
#	condor/agents/risk.py
#	condor/preferences.py
#	handlers/agents/_shared.py
#	handlers/agents/confirmation.py
…cation scripts

A quote now carries `approximation`. The tool already returned the result dict
unchanged, so the field arrived on its own — what was missing is any reason for
the model reading it to care. A BUY is an ExactOut order, and a thin token with
no ExactOut route is quoted by pricing the sell leg and quoting that input
forward, about 2.5%. Nobody is overcharged; the order is silently resized, which
is exactly what a user who asked for a specific quantity needs told.

The two scripts sit beside their tracked siblings: test_executors.py drives the
fixes that only exist at the executor layer (the slippage ramp is executor state,
not a Gateway feature), and test_pancakeswap_sol_clmm.py establishes that a
position's fees are non-zero BEFORE closing it — a close reporting 0/0 is
indistinguishable from a position that earned nothing, which is how GW-18
survived.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
The guide told every caller to apply a x 0.995 haircut to
executed_amount_base. On an EXACT fill that is simply wrong and under-spends
by half a percent: a SOL-USDC round trip on 2026-08-21 moved gross
±0.010000000 SOL in both directions, matching the request to the lamport.

The shortfall it was written for is real but conditional — it happens when
a BUY was APPROXIMATED, which the quote now says outright via the
`approximation` flag. A blanket haircut is the wrong remedy for a
conditional problem, because it is wrong in the common case to be safe in
the rare one.

Also documents what custom_info now carries: transaction_hash (order_id is
internal and appears nowhere on chain), and the live slippage_pct, which
reads above the configured start exactly when a widening has happened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
…hat work

`list_pools` defaulted to sort_key="volume". On a token whose DLMM pools are
all idle every row ties at volume_24h = 0.00, so the order is arbitrary and
liquidity is never consulted. On UMBRA-USDC that put a pool holding $1.07
above one holding $15.34K and buried the deep one at row 68 of 73 — and an
agent reading top-down took the $1.07 pool and reported the other as "not
found".

Volume ranks pools by how much OTHERS traded; the LP question is how much
depth is there.

The docstring also advertised "volume, tvl, feetvlratio, etc." — feetvlratio
and liquidity both 400'd upstream and surfaced as an opaque 500. It now names
the keys each connector actually accepts, and hummingbot-api refuses anything
else with the legal list instead of letting it fail at the DEX.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GuR4iDtH4ZfT51rJfs3DhK
@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
@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown

Too many files changed for review (252 files, 100 file limit).

Bypass the limit by tagging @greptile-apps to review.

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

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

3 participants