Repository navigation
Conversation
…ine mode The MCP server connected to the Remote Script exactly once, at startup. If Live was not running yet (the usual case when the MCP host launches the server first), createBridgeRuntime() returned an offline bridge that rejected every tool call for the lifetime of the process with the cached startup error, even after Live came up. The same happened after a Live restart, because the TCP client's own reconnect loop gave up after one failed attempt. bridge-runtime: - Every bridge.call() checks the current client and, when it is not connected, opens a fresh TcpJsonRpcClient and repeats the handshake before forwarding the call. - Concurrent calls share a single in-flight reconnect attempt. - Failed attempts are rate limited (reconnectCooldownMs, default 1s) so a burst of calls while Live is closed does not hammer the socket. - New onClient() hook lets index.ts re-attach the notification forwarder to whichever client is current; the previous one is detached. tcp-client: - scheduleReconnect() now reschedules itself after a failed attempt. Previously it called handleDisconnect(), which returns early when the state is already "disconnected" with no socket, ending the backoff loop after the first failure. Tests: 5 new cases (startup-offline recovery, cooldown, client replacement with subscriber hand-off, single-flight reconnect, and the tcp-client retry loop). Each fails without the fix; full suite 198 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JehnFxtQ4wX3KSf7Lc2keo
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: Comment |
|
Tip For best results, initiate chat on the files or code changes.
You are interacting with an AI system. |
Pantani
left a comment
There was a problem hiding this comment.
Thanks for this — the core fix works. I checked out the branch: typecheck, lint and npm test pass, and a real-socket test (server starts with Live down → Live comes up → Live restarts) reconnects correctly and moves event.* notifications to the new client. A few things need to change before merge, though.
Blocking
1. TcpJsonRpcClient: close() during an in-flight reconnect now loops forever (src/live-client/tcp-client.ts:418)
connect()'s onError sets this.state = "disconnected", overwriting "closed" if close() ran while the attempt was in flight. The new guard this.state !== "closed" then passes and scheduleReconnect() keeps going forever (timer keeps the process alive, log spam). Before this PR the same race leaked a single attempt; now it is unbounded.
Repro (reconnect attempts went 4 → 21 within 100 ms after close()):
it("stops reconnecting after close() lands mid-connect", async () => {
const sockets: net.Socket[] = [];
const srv = net.createServer((s) => { sockets.push(s); s.on("error", () => {}); });
await new Promise<void>((r) => srv.listen(0, "127.0.0.1", () => r()));
const port = (srv.address() as net.AddressInfo).port;
const client = new TcpJsonRpcClient({ host: "127.0.0.1", port, reconnectInitialMs: 5, reconnectMaxMs: 5 });
await client.connect();
const c = client as unknown as { reconnectAttempt: number; connect: () => Promise<void> };
const orig = c.connect.bind(client);
let done = false;
c.connect = () => { const p = orig(); if (!done) { done = true; void client.close(); } return p; };
srv.close(); sockets.forEach((s) => s.destroy());
await new Promise((r) => setTimeout(r, 30));
const n = c.reconnectAttempt;
await new Promise((r) => setTimeout(r, 100));
expect(c.reconnectAttempt).toBe(n); // fails today
});The same race on the success path is pre-existing but worth fixing together: onConnect sets state = "connected" after close(), leaking an open socket. Suggested fix: in both onConnect and onError, don't overwrite "closed" — if the client was closed meanwhile, sock.destroy() and reject. Note the new "keeps retrying after a failed reconnect attempt" test calls close() while the loop is running and can hit exactly this race.
(Side note: after this PR no production path uses autoReconnect: true anymore — the runtime, doctor and chat all pass false — so this change is a library fix, not part of the server fix. Fine to keep, but it must not regress close().)
Should fix (here or in a follow-up)
2. ableton-mind chat still locks into offline mode (src/cli/chat.ts:161-188)
createChatContext() connects once at startup with autoReconnect: false. If Live isn't running yet, or restarts during the session, every tool call fails until the chat process is restarted — the exact bug this PR fixes for the MCP server. docs/guide/local-copilot.md:61 implies it recovers once the Remote Script is active. It also throws a plain Error instead of JsonRpcTransportError. Reusing createBridgeRuntime() there would fix it.
3. No connect timeout (src/live-client/tcp-client.ts:187)
With on-demand reconnect, any tool call past the cooldown can trigger net.createConnection with no timeout, and concurrent calls share that in-flight attempt. With ABLETON_MIND_HOST pointing at an unreachable host (Docker / host.docker.internal setups), calls can hang for the OS SYN timeout (tens of seconds) instead of failing fast. Previously this only affected startup. The per-call timeoutMs doesn't cover connect + handshake either. A connect timeout (e.g. reuse defaultTimeoutMs) would close this.
Minor
- Existing tests now depend on the wall clock (
tests/server-bridge-runtime.test.ts:7and:48): they rely on the call landing inside the real 1 s cooldown. On a slow CI runner past 1 s, the default factory opens a real socket to127.0.0.1:9876; with Live running on a dev machine, the first test (handshakevi.fn()resolves) would callsession.get_infoon real Live. Please passclientFactoryandnowin those tests. onClientwith the same function twice overwrites theMapentry without running the previous cleanup, leaking the first listener.- Leftovers: header comment in
src/index.ts:6and:14still describes "offline mode";createOfflineBridgeClientis now unused.
|
@jwilkins any update? |
Problem
The MCP server only tries to reach the AbletonMind Remote Script once, at startup. If Live isn't running at that moment — the normal case when Claude Desktop / Cursor launches MCP servers on app start —
createBridgeRuntime()returns an offline bridge and every tool call fails for the life of the process:…even after Live is up and the bridge is listening. The only workaround is restarting the MCP host after Live has fully loaded. Restarting Live mid-session has the same effect, because
TcpJsonRpcClient's auto-reconnect loop stops after the first failed attempt.Fix
src/server/bridge-runtime.ts: the runtime now reconnects on demand.bridge.call()checks the current client; if it isn't connected it creates a freshTcpJsonRpcClient, runs the handshake, then forwards the call.reconnectCooldownMs(default 1 s); calls inside the window fail fast with the last error.onClient()hook soindex.tsre-attaches the notification forwarder to whichever client is current (and detaches it from the old one).src/live-client/tcp-client.ts:scheduleReconnect()reschedules itself after a failed attempt. It used to callhandleDisconnect(), which returns early when the state is already"disconnected"with no socket — so the backoff loop ran exactly once.Tests
5 new tests, each verified to fail without the fix:
TcpJsonRpcClientkeeps retrying after a failed reconnecttsc --noEmit,biome check,deps:validate,complexityall clean;vitest run: 198 passed, 4 skipped.Repro
session_get_info.Ableton bridge offline: … ECONNREFUSEDforever.🤖 Generated with Claude Code
https://claude.ai/code/session_01JehnFxtQ4wX3KSf7Lc2keo