vercel/ai · error
A host tool relay turn is already active.
Error message
A host tool relay turn is already active.
What it means
startHostToolRelay's returned relay can only be bound to a single active ACP prompt turn at a time. bindTurn throws this plain Error when another turn is currently bound and the incoming turn is a different one. Re-binding the same turn is allowed, and unbindTurn clears the slot, so the guard enforces one-turn-at-a-time semantics for the shared local HTTP relay.
Source
Thrown at packages/harness-acp/src/v1/bridge/host-tool-relay.ts:114
} catch (error) {
const status = error instanceof RelayRequestError ? error.status : 500;
response.writeHead(status, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
error: error instanceof Error ? error.message : String(error),
}),
);
}
});
await listen({ server });
const address = server.address() as AddressInfo;
return {
url: `http://127.0.0.1:${address.port}/invoke`,
credential,
bindTurn: ({ turn }) => {
if (activeTurn != null && activeTurn !== turn) {
throw new Error('A host tool relay turn is already active.');
}
activeTurn = turn;
},
unbindTurn: ({ turn }) => {
if (activeTurn === turn) activeTurn = undefined;
},
updateCatalog: ({ tools: nextTools }) => {
const fingerprint = catalogFingerprint({ tools: nextTools });
if (fingerprint === state.fingerprint) {
return { changed: false, revision: state.revision };
}
state.tools = [...nextTools];
state.fingerprint = fingerprint;
state.revision += 1;
resolveCatalogChanges({ state });
return { changed: true, revision: state.revision };
},
waitForCatalogRefresh: ({ revision, timeoutMs }) =>View on GitHub (pinned to 69428b1f8b)
Solutions
- Always call relay.unbindTurn({ turn }) in a finally block when a turn ends, so the slot is freed.
- Check whether a turn is already active and wait for it to complete (or abort it) before binding a new turn.
- Create a separate relay via startHostToolRelay for each concurrent session/turn instead of sharing one.
- If re-binding the same turn, note the guard permits it — ensure you pass the same HostToolRelayTurn object, not an equal-looking new one.
Example fix
// before
session.onTurnStart(turn => {
relay.bindTurn({ turn });
});
// after
session.onTurnStart(turn => {
relay.bindTurn({ turn });
});
session.onTurnEnd(turn => {
relay.unbindTurn({ turn }); // always release, even on error
}); Defensive patterns
Strategy: try-catch
Validate before calling
// Track the currently bound turn before binding
let boundTurn: HostToolRelayTurn | undefined;
function safeBind(relay: HostToolRelay, turn: HostToolRelayTurn) {
if (boundTurn != null && boundTurn !== turn) {
throw new Error('Cannot bind: another relay turn is already active.');
}
relay.bindTurn({ turn });
boundTurn = turn;
} Type guard
function canBind(activeTurn: HostToolRelayTurn | undefined, next: HostToolRelayTurn): boolean {
return activeTurn == null || activeTurn === next;
} Try / catch
try {
relay.bindTurn({ turn });
await runPromptTurn(turn);
} catch (error) {
if (error instanceof Error && error.message === 'A host tool relay turn is already active.') {
await waitForActiveTurnToEnd(); // or abort the active turn
relay.bindTurn({ turn });
} else {
throw error;
}
} finally {
relay.unbindTurn({ turn });
} Prevention
- Always pair bindTurn with unbindTurn in a finally block.
- Use one relay instance per concurrent session instead of sharing.
- Serialize turn starts behind a queue/mutex so only one bind happens at a time.
- On abort/error paths, verify unbindTurn actually ran (assert activeTurn cleared in tests).
When it happens
Trigger: Calling relay.bindTurn({ turn }) while activeTurn is set to a different HostToolRelayTurn — e.g. binding a second ACP session's turn before the first session unbinds, or binding a new turn in a turn-start hook while a previous turn was never unbound (an earlier turn errored or was aborted without cleanup).
Common situations: Running multiple concurrent ACP prompt turns against one shared relay; a crashed or aborted turn leaking its binding because unbindTurn was never called; forgetting to unbind in a finally block when bridging host tool calls; reusing a single relay across several sessions instead of one relay per session.
Related errors
- No ACP prompt turn is active.
- Host tool ${body.toolName} was invoked from stale catalog re
- Unknown host tool relay endpoint.
- Invalid host tool relay credential.
- Invalid host tool catalog poll request.
AI-assisted analysis of vercel/ai@69428b1f8b (2026-08-30).
Data as JSON: /api/errors/8744a85fadd6ba53.
Report an issue: GitHub.