tinyhumansai/openhuman · error

[tunnel] no session cipher — handshake incomplete

Error message

[tunnel] no session cipher — handshake incomplete

What it means

sendEnvelope() requires this.cipher, the symmetric XChaCha20-Poly1305 TunnelCipher that exists only after the sealed handshake is acked by the core. The socket 'disconnect' handler nulls cipher (along with staticDhKey and clientEphemeralKeypair), so the error means: a call was attempted before the first handshake completed, or after a disconnect before the automatic reconnection re-ran the handshake.

Solutions

  1. Always go through the transport's connect path (or the TransportManager race) and await readiness before the first call
  2. On this error, await the transport's connect/handshake again (ensureConnected re-runs it because disconnect nulled _connectPromise) then retry the call
  3. Call manager.reset() to rebuild both transports if the tunnel keeps flapping

Example fix

// before
this.socket.emit(...); // raw send, cipher may be null after a disconnect
// after
await this.ensureConnected(); // re-runs handshake, re-derives cipher
await this.sendEnvelope(envelope);
Defensive patterns

Strategy: validation

Validate before calling

// Handshake-readiness gate: never emit frames directly — go through call(),
// which awaits the connect/handshake promise before sending:
const result = await tunnel.call('openhuman.ping', {}); // connect + handshake + RPC
// If you drive the socket yourself, mirror the internal state:
if (!tunnel.isConnected()) await tunnel.connect();

Try / catch

try {
  await tunnel.call(method, params);
} catch (err) {
  if (err instanceof Error && err.message.includes('no session cipher')) {
    await sleep(500);              // allow socket.io reconnect + re-handshake
    return await tunnel.call(method, params);
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling transport.call() without awaiting the connect/handshake promise first; a socket.io disconnect (network blip, laptop sleep) landing between calls — the next sendEnvelope hits a nulled cipher until reconnect + tunnel:connected + handshake ack complete again.

Common situations: Race at startup where UI issues RPC before the tunnel is ready; mobile clients backgrounding the app (socket drops) and resuming instantly; flaky Wi-Fi causing rapid disconnect/reconnect cycles.

Understand the failure class

Related errors


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

Appendix: 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 7491200858)