Skip to content

disconnect() fails during Authorizing state - connection continues after disconnect #18

@kmalmur

Description

@kmalmur

Calling disconnect() while the client is in the Authorizing state (i.e., during the startBot() HTTP request) fails with TransportNotInitialized and the connection proceeds anyway.

Root cause

startBotAndConnect() chains startBot().chain { connect(it) }. During startBot(), the transport state is Authorizing but DailyTransport.call (CallClient) is null — it's only created in initDevices(), which runs as part of connect().

When disconnect() is called in this state:

  1. PipecatClient.disconnect() delegates to DailyTransport.disconnect()
  2. DailyTransport.disconnect() calls withCall { ... } which returns RTVIError.TransportNotInitialized because call is null
  3. release() is also a no-op (call?.release() — call is null)
  4. When startBot() HTTP response arrives, it sets state to Authorized and the chain calls connect(), which creates a new CallClient, joins the Daily room, and the bot connects and starts speaking

There is no way to prevent this from the caller side when using startBotAndConnect(), since the chain is internal.

Expected behavior

disconnect() should succeed in any state and prevent any pending connection from proceeding. Specifically:

  • PipecatClient.disconnect() should handle Authorizing/Authorized states by setting state to Disconnected directly (no transport teardown needed)
  • startBot() withCallback should not override Disconnected with Authorized after the HTTP response arrives
  • connect() should check state before proceeding and reject if Disconnected

Suggested fix

PipecatClient.ktdisconnect()

fun disconnect(): Future<Unit, RTVIError> = thread.runOnThreadReturningFuture {
    when (transport.state()) {
        TransportState.Authorizing,
        TransportState.Authorized -> {
            transport.setState(TransportState.Disconnected)
            connection?.ready?.resolveErr(RTVIError.OperationCancelled)
            connection = null
            resolvedPromiseOk(thread, Unit)
        }
        TransportState.Disconnected -> resolvedPromiseOk(thread, Unit)
        else -> transport.disconnect()
    }
}

PipecatClient.ktstartBot() withCallback

.withCallback {
    // Don't override Disconnected state — disconnect() may have been
    // called while the HTTP request was in-flight.
    if (transport.state() != TransportState.Disconnected) {
        transport.setState(
            if (it.ok) TransportState.Authorized else TransportState.Disconnected
        )
    }
}

PipecatClient.ktconnect() early check

fun connect(transportParams: ConnectParams): Future<Unit, RTVIError> =
    thread.runOnThreadReturningFuture {
        if (transport.state() == TransportState.Disconnected) {
            return@runOnThreadReturningFuture resolvedPromiseErr(
                thread, RTVIError.OperationCancelled
            )
        }
        // ... existing code
    }

DailyTransport.ktdisconnect() (defensive)

override fun disconnect(): Future<Unit, RTVIError> = thread.runOnThreadReturningFuture {
    val currentClient = call
    if (currentClient == null) {
        setState(TransportState.Disconnected)
        transportContext.onConnectionEnd()
        return@runOnThreadReturningFuture resolvedPromiseOk(thread, Unit)
    }
    // ... existing callClient.leave() code
}

Workaround

Use startBot() and connect() separately instead of startBotAndConnect(), with a disconnect check between them:

val connectParams = client.startBot(apiRequest).await()

if (disconnected) {
    client.release()
    return
}

client.connect(connectParams).await()

Environment

  • ai.pipecat:daily-transport:1.1.0
  • Android

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions