tinyhumansai/openhuman · error · Error

[tunnel] no session cipher — handshake incomplete

Error message

[tunnel] no session cipher — handshake incomplete

What it means

sendEnvelope seals every frame with this.cipher, which exists only after the encrypted handshake completes and both sides derived the session key. This invariant fires when a request is sent before connect() resolved, or on a transport whose handshake failed and was reused anyway.

Source

Thrown at app/src/services/transport/TunnelTransport.ts:322

        this.streams.delete(requestId);
      }
      return;
    }

    if (kind === 'response') {
      const pending = this.pending.get(requestId);
      if (!pending) return;
      clearTimeout(pending.timeoutId);
      this.pending.delete(requestId);
      pending.resolve(envelope.payload);
      return;
    }
  }

  // -- send ------------------------------------------------------------------

  private async sendEnvelope(envelope: Envelope): Promise<void> {
    if (!this.cipher) throw new Error('[tunnel] no session cipher — handshake incomplete');

    await this.rateLimiter.consume();

    const chunks = chunk(envelope);
    for (const raw of chunks) {
      const encrypted = this.cipher.seal(raw);
      const frameB64 = base64urlEncode(encrypted);
      this.socket!.emit('tunnel:frame', { channelId: this.channelId, payload: frameB64 });
    }

    log(
      '[tunnel] sent %s requestId=%s chunks=%d',
      envelope.kind,
      envelope.requestId,
      chunks.length
    );
  }

View on GitHub (pinned to a221052e0d)

Solutions

  1. Always await connect() before the first call, or route through TransportManager which pings first
  2. On handshake/connect failure, close and discard the transport and build a fresh one
  3. Queue early requests until the handshake completes instead of sending immediately

Example fix

// before
void transport.connect();
await transport.call('openhuman.ping', {});

// after
await transport.connect();
await transport.call('openhuman.ping', {});
Defensive patterns

Strategy: validation

Validate before calling

// Gate sends on a completed round-trip instead of assuming connection:
const t = await manager.getTransport(); // ping already succeeded → handshake done
await t.call('openhuman.ping', {});

Try / catch

Catch 'handshake incomplete', close() the broken instance, construct a fresh transport, await connect(), and retry the request once — never resend on the same instance.

Prevention

When it happens

Trigger: Calling call()/request() on a TunnelTransport whose connect() promise has not resolved (or has rejected) — fire-and-forget connect() followed immediately by send; reconnect logic that swaps the socket but not the cipher.

Common situations: Missing `await transport.connect()`; racing requests fired right after construction; reusing a transport instance after a connect_error.

Understand the failure class

Related errors


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/30cc10288c7abe03. Report an issue: GitHub.