GEMINI LABEN
CLI — 本日6/18、Gemini CLIとGemini Code Assist IDE拡張がAI Pro/Ultra・無料個人利用向けにリクエスト提供を終了。後継はAntigravity CLIですFLASH — Gemini 3.5シリーズが始動し3.5 Flashが提供開始。エージェントとコーディング向けにフロンティア級、長時間タスクに強いと説明されていますDEEPTHINK — Gemini 3 Deep ThinkがGoogle AI Ultra向けに展開中。数学・科学・論理・多段推論の最上位モードですAPP — GeminiアプリにDaily Brief、再設計UI、AI動画モデルGemini Omni、個人AIエージェントGemini Sparkが加わりましたDESIGN — 新デザイン言語Neural Expressiveで、よりリッチな視覚出力とモダリティ間の素早い切替に向けて再構築されていますULTRA — Google AI Ultraはモデル最上位アクセス・Deep Research・Veo 3動画生成・100万トークンコンテキストを束ねるプレミアム枠ですCLI — 本日6/18、Gemini CLIとGemini Code Assist IDE拡張がAI Pro/Ultra・無料個人利用向けにリクエスト提供を終了。後継はAntigravity CLIですFLASH — Gemini 3.5シリーズが始動し3.5 Flashが提供開始。エージェントとコーディング向けにフロンティア級、長時間タスクに強いと説明されていますDEEPTHINK — Gemini 3 Deep ThinkがGoogle AI Ultra向けに展開中。数学・科学・論理・多段推論の最上位モードですAPP — GeminiアプリにDaily Brief、再設計UI、AI動画モデルGemini Omni、個人AIエージェントGemini Sparkが加わりましたDESIGN — 新デザイン言語Neural Expressiveで、よりリッチな視覚出力とモダリティ間の素早い切替に向けて再構築されていますULTRA — Google AI Ultraはモデル最上位アクセス・Deep Research・Veo 3動画生成・100万トークンコンテキストを束ねるプレミアム枠です
記事一覧/API / SDK
API / SDK/2026-06-16上級

Gemini Live API のセッションが切れるたびに会話が消える — 期限切れトークンの再取得とセッション再開の実装メモ

Gemini Live API の WebSocket が再接続するたびに会話履歴とユーザーの発話を取りこぼす問題を、エフェメラルトークンの単発消費・セッション再開ハンドル・goAway 予告の3点から実装で塞ぐ運用メモです。

Gemini Live API4WebSocket3エフェメラルトークンセッション再開再接続リアルタイム3

プレミアム記事

電車がトンネルに入った数秒で WebSocket が切れ、戻ってきたら AI が会話の冒頭を忘れている。個人開発で作っていた Gemini Live API の音声アシスタントは、デモでは完璧に動いていました。それを実機に載せた途端、私自身こうした取りこぼしに毎日のように悩まされました。

切断そのものは避けられません。モバイル回線は切れますし、Live API のセッションにも上限があります。プロダクションで問われるのは「切れた後にどう戻るか」です。ここがデモと本番運用を分ける落とし穴でした。多くの実装が再接続のコードまでは書いているのに、戻った瞬間に二つのものを失っています。ひとつは認証、もうひとつは会話の文脈です。この二つを落とさない再接続を、実際に詰まった順番で組み立てていきます。

なお Live API のモデル指定は、2026 年 6 月に一般提供となった gemini-2.5-flash 系を前提にしています。gemini-2.0-flash を使っている場合は、ここで扱う再接続の作り込みとあわせてモデル ID も移行しておくのが無難です。

再接続が認証を落とす — トークンは使い回せない

最初にぶつかったのは、再接続のたびに 401 で弾かれる現象でした。初回接続は通るのに、二回目以降だけ失敗します。

原因はエフェメラルトークンの性質にあります。バックエンドが発行する短命トークンは、接続を一度確立すると消費されます。つまり、一本のトークンで張れる WebSocket は原則ひとつです。再接続のコードが「最初に取得したトークンを変数に保持して再利用する」設計になっていると、二回目の接続で使用済みのトークンを送ってしまい、認証に失敗します。

正しい順序は単純です。再接続を試みるたびに、バックエンドからトークンを取り直すこと。トークン発行 API はこう組みます。

// app/api/live/token/route.ts
import { NextRequest, NextResponse } from "next/server";
 
const TOKEN_ENDPOINT =
  "https://generativelanguage.googleapis.com/v1alpha/ephemeralTokens:create";
 
export async function POST(req: NextRequest) {
  // 自社の認証を必ず先に通す。ここを抜くと誰でもトークンを引ける踏み台になる
  const session = await getServerSession(req);
  if (!session?.user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
 
  const apiKey = process.env.GEMINI_API_KEY;
  if (!apiKey) {
    return NextResponse.json({ error: "Server misconfigured" }, { status: 500 });
  }
 
  const res = await fetch(`${TOKEN_ENDPOINT}?key=${apiKey}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      // 接続確立の猶予(newSessionExpireTime)と、セッション全体の上限(expireTime)を分けて指定する
      uses: 1,
      expireTime: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
      newSessionExpireTime: new Date(Date.now() + 60 * 1000).toISOString(),
      liveConnectConstraints: {
        model: "models/gemini-2.5-flash",
        config: {
          responseModalities: ["AUDIO"],
          systemInstruction: {
            parts: [{ text: "あなたはこのアプリ専用のアシスタントです。" }],
          },
        },
      },
    }),
  });
 
  if (!res.ok) {
    console.error("token issue failed:", await res.text());
    return NextResponse.json({ error: "Failed to issue token" }, { status: 502 });
  }
 
  const data = await res.json();
  return NextResponse.json({ token: data.name, expiresAt: data.expireTime });
}

ここで効くのが newSessionExpireTime です。トークン全体の寿命(expireTime)とは別に、「最初の接続を張るまでの猶予」を短く切れます。トークンが漏れても、攻撃者が新規セッションを開ける窓は数十秒に限られます。発行直後にすぐ繋ぐ前提なら、この猶予は 1 分もあれば十分です。

クライアント側は、トークンを保持せず「接続の直前に取りに行く関数」として渡します。

// 接続を張る側には、トークンそのものではなく「取得関数」を渡す
const getToken = async (): Promise<string> => {
  const res = await fetch("/api/live/token", { method: "POST" });
  if (!res.ok) throw new Error("token fetch failed");
  const { token } = await res.json();
  return token;
};

この一段で 401 ループは消えます。トークンは状態として持たず、必要になった瞬間に毎回新しく引く。これが再接続を前提にした認証の基本姿勢です。

セッション再開ハンドル — 文脈を失わずに戻る

認証が直っても、まだ問題は残ります。再接続には成功するのに、戻ってきた AI が直前の会話を覚えていないのです。

新しい WebSocket を張り直すと、Live API から見れば別の真新しいセッションです。setup メッセージだけ送って繋ぎ直すと、それまでのターンはすべて失われます。これを防ぐのが**セッション再開(session resumption)**です。

仕組みはこうです。接続中、サーバーは定期的に sessionResumptionUpdate というメッセージを送ってきます。この中の newHandle が、現在のセッション状態を指す引換券です。クライアントはこれを常に最新の状態で保持しておき、再接続時に setup の中で渡します。すると Live API は新しい接続に古い文脈を引き継いでくれます。

最新ハンドルの保持と、再開つき setup の送信を一箇所にまとめます。

// lib/LiveSession.ts
type Json = Record<string, unknown>;
 
export class LiveSession {
  private ws: WebSocket | null = null;
  private resumptionHandle: string | null = null;  // 最新の再開ハンドル
  private attempts = 0;
  private closedByUser = false;
  private readonly maxAttempts = 6;
  private readonly base = 1000;
 
  constructor(
    private readonly getToken: () => Promise<string>,
    private readonly wsBase: string,
    private readonly onMessage: (m: Json) => void,
  ) {}
 
  async connect() {
    this.closedByUser = false;
    await this.open();
  }
 
  private async open() {
    const token = await this.getToken();  // 毎回新しいトークンを引く
    const ws = new WebSocket(`${this.wsBase}?access_token=${token}`);
 
    ws.onopen = () => {
      this.attempts = 0;
      ws.send(JSON.stringify({
        setup: {
          model: "models/gemini-2.5-flash",
          generationConfig: { responseModalities: ["AUDIO"] },
          // ハンドルがあれば渡す。初回は handle 省略で新規開始
          sessionResumption: this.resumptionHandle
            ? { handle: this.resumptionHandle }
            : {},
        },
      }));
    };
 
    ws.onmessage = (e) => {
      const msg = JSON.parse(e.data) as Json;
      // 再開ハンドルは届くたびに上書きして最新を保つ
      const update = msg.sessionResumptionUpdate as
        | { resumable?: boolean; newHandle?: string }
        | undefined;
      if (update?.resumable && update.newHandle) {
        this.resumptionHandle = update.newHandle;
      }
      // サーバーからの切断予告は先回り再接続のトリガーにする
      if ("goAway" in msg) {
        this.reconnectSoon();
        return;
      }
      this.onMessage(msg);
    };
 
    ws.onclose = () => {
      this.ws = null;
      if (!this.closedByUser) this.reconnectSoon();
    };
    ws.onerror = (err) => console.error("ws error", err);
    this.ws = ws;
  }
 
  private reconnectSoon() {
    if (this.attempts >= this.maxAttempts) {
      console.error("再接続の上限に到達。利用者に手動再開を促す");
      return;
    }
    // 指数バックオフ + ジッター: 1s, 2s, 4s, ... に乱数を足して同時殺到を避ける
    const delay = this.base * 2 ** this.attempts + Math.random() * 500;
    this.attempts++;
    setTimeout(() => this.open(), delay);
  }
 
  send(data: Json): boolean {
    if (this.ws?.readyState !== WebSocket.OPEN) return false;  // 切断中は黙って捨てる
    this.ws.send(JSON.stringify(data));
    return true;
  }
 
  disconnect() {
    this.closedByUser = true;
    this.ws?.close();
    this.ws = null;
  }
}

実装で気をつける点が二つあります。

ひとつは、ハンドルは「届くたびに」更新すること。sessionResumptionUpdate は会話が進むほど新しい状態を指す券に差し替わります。古いハンドルを掴んだままだと、再開はできても文脈が数ターン前まで巻き戻ります。最新で上書きし続けるのが正解です。

もうひとつは、resumablefalse のときは握らないこと。サーバーはまだ再開点が確定していない瞬間にも更新を送ってきます。そのときの券で再開しようとすると失敗するので、resumable が真でハンドルが入っているときだけ保存します。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
再接続時にトークンを使い回して 401 で落ちる典型パターンと、発行直後に取得し直す具体的な実装コード
sessionResumption ハンドルで切断前の文脈を引き継ぎ、ユーザーの発話途中でも会話を失わない再開フロー
goAway を無視して突然切れる事故を、予告受信から先回り再接続につなげる状態機械の組み方
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Gemini Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

開発ツール2026-05-05
Gemini Live API を Expo アプリに組み込む — リアルタイム音声会話機能の実装ガイド
Gemini Live API を Expo(React Native)のモバイルアプリに組み込み、リアルタイム音声会話機能を実装する方法を解説します。WebSocket 接続・音声録音・再生の実践的なコード例付き。
開発ツール2026-05-03
Gemini Live API で音声 SaaS を構築する実装 — リアルタイム会話と Stripe 課金の連動
Gemini Live API を使ったリアルタイム音声 SaaS の本番実装ガイド。WebSocket 接続から Stripe 連動の利用時間課金まで、個人開発者がゼロから作り切るための完全な設計と実装コードを公開します。
API / SDK2026-04-04
Gemini Live APIで音声エージェントを作る入門
Gemini Live APIを使ってリアルタイム音声会話ができる音声エージェントを構築する方法を解説。APIセットアップから実装例まで。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →