Cursorはチャット履歴をどこに保存している?SQLiteから見るローカル保存の仕組み

Lab

この記事で分かること

  • SQLiteとは何か と、アプリ内部でよく使われる理由が分かります。
  • 迷子になりがちなAIエディタCursorの過去チャットはどこに保存されているか を、実際の調査ベースで理解できます。
  • state.vscdb と agent-transcripts の役割の違いを知り、バックアップで何を残すべきか が整理できます。
  • 「SQLiteに本文がそのまま入っているのか?」のような疑問に対して、確認できたことと未確定なこと を分けて把握できます。

はじめに:Cursorのチャットはどこにあるのか

AIエディタCursor を日常的に使っていると、過去のチャットが保存されていない事があります。そこで気になるのは 「チャット履歴はどこに保存されているのか」 という点です。

実はCuesorでは作業したフォルダごとに過去チャット表示されます。つまり別のフォルダで作業すると過去チャットは表示されないのです。さらに調べていくと、state.vscdb というファイルが見つかります。これは SQLite ベースのデータベースで、Cursor の状態保存に関わっていそうです。
ただし、そこで注意したいのは、SQLite が見つかったからといって、チャット本文がそのまま平文で入っているとは限らない ことです。

この記事では、まず SQLite の基本を初心者向けに押さえたうえで、Windows 環境の Cursor を例に、

  • state.vscdb とは何か
  • globalStorage と workspaceStorage の違い
  • agent-transcripts には何が入っていたか
  • バックアップするときに何を意識するとよいか

を順番に整理します。


1. SQLiteとは何か

SQLite は、サーバーを別に立てなくても1ファイルで扱える軽量なデータベース です。

MySQL や PostgreSQL のように「DBサーバーに接続して使う」タイプとは違い、アプリが自分の内部データを保存する用途でもよく使われます。
そのため、Webサービスの本番DBとしてだけでなく、エディタ、ブラウザ、デスクトップアプリの内部保存でも登場します。

1-1. SQLiteがよく使われる場面

  • デスクトップアプリの設定や状態の保存
  • エディタや開発ツールのキャッシュ、メタデータ管理
  • ブラウザやローカルツールの小規模データ保存

この記事で扱う Cursor の state.vscdb も、まさにこの 「アプリ内部の状態保存」 の文脈で見ると理解しやすくなります。

1-2. SQLiteの特徴

  • 1つの DB ファイルを中心に扱える
  • SQL で中身を確認しやすい
  • 状況によっては -wal や -shm などの補助ファイルも一緒に使う

特に今回の調査で重要なのが、SQLite は本体ファイルだけで完結していないことがある 点です。
バックアップや復元を考えるなら、このあと出てくる state.vscdb-wal や state.vscdb-shm も意識する必要があります。


2. CursorではどこにSQLiteがあるのか

今回の調査では、Cursor のローカル保存先として主に次の SQLite ファイルが見つかりました。

  • %APPDATA%\Cursor\User\globalStorage\state.vscdb
  • %APPDATA%\Cursor\User\workspaceStorage\{workspaceHash}\state.vscdb

また、同じ場所に次の補助ファイルが存在することがあります。

  • state.vscdb-wal
  • state.vscdb-shm

2-1. 今回確認した主な保存先

まず注目したのは globalStorage 側の state.vscdb です。
こちらには bubbleId:* のようなキーが多数あり、会話まわりの情報が集まっていそう だと分かりました。

一方で、workspaceStorage 側も確認しましたが、今回の範囲では 会話本体らしき bubbleId:% は見つかりませんでした
そのため、少なくともこの環境では、チャット関連の主要データは globalStorage 側に寄っている可能性が高い と考えられます。

2-2. state.vscdb はどんなDBか

調査時点で確認できた主要テーブルは、少なくとも次の2つです。

CREATE TABLE ItemTable (
  key TEXT UNIQUE ON CONFLICT REPLACE,
  value BLOB
);

CREATE TABLE cursorDiskKV (
  key TEXT UNIQUE ON CONFLICT REPLACE,
  value BLOB
);

見た目としては、Key-Value ストアに近い構造 です。

  • key に bubbleId:... や composerData:... のような文字列が入る
  • value は BLOB で、JSON のこともあれば別形式のこともある

つまり、一般的な「users テーブル」「orders テーブル」のような業務DBとは違って、アプリ内部の状態や参照情報をまとめて持つ用途 で使われていると考えると分かりやすいです。

2-3. 実際にSQLを投げると何が返るか

ここは、「SQLite の DB ファイルと SQL が分かると、アプリ内部で何を持っているかかなり覗ける」 ことを示しやすい部分です。
実際に今回の環境では、次のファイルが存在していました。

C:\Users\Kei\AppData\Roaming\Cursor\User\globalStorage\state.vscdb
C:\Users\Kei\AppData\Roaming\Cursor\User\globalStorage\state.vscdb-wal
C:\Users\Kei\AppData\Roaming\Cursor\User\globalStorage\state.vscdb-shm
C:\Users\Kei\AppData\Roaming\Cursor\User\workspaceStorage\8297fdca99af397de1d34f6d27d9c797\state.vscdb

サイズも確認できました。

state.vscdb      : 474,783,744 bytes
state.vscdb-wal  :   4,136,512 bytes
state.vscdb-shm  :      32,768 bytes
workspace state  :     438,272 bytes

実際に state.vscdb のようなファイルへ SQL を投げる方法自体は、別記事の
ローカルPCにSQLiteを導入してSQLを実行する手順 を前提にします。

ここでは実行環境のセットアップ説明は繰り返さず、Cursor の内部 DB に対してどんな SQL を投げると何が見えるか に絞って進めます。
大事なのは、state.vscdb を触るときは 必ず読み取り専用の前提で扱う ことです。

そのうえで、たとえば globalStorage\state.vscdb に次の SQL を投げると、まずテーブル一覧が分かります。

SELECT name
FROM sqlite_master
WHERE type = 'table'
ORDER BY name;

返ってきた結果はこうでした。

ItemTable
cursorDiskKV

さらに、会話関連に見えるキーの件数を数えると、次のようになりました。

SELECT
  SUM(CASE WHEN key LIKE 'bubbleId:%' THEN 1 ELSE 0 END) AS bubble_count,
  SUM(CASE WHEN key LIKE 'composerData:%' THEN 1 ELSE 0 END) AS composer_count,
  SUM(CASE WHEN key LIKE 'messageRequestContext:%' THEN 1 ELSE 0 END) AS message_context_count
FROM cursorDiskKV;

調査時点のこの環境での結果:

bubble_count | composer_count | message_context_count
11518        | 73             | 820

この時点で、少なくとも cursorDiskKV の中に、会話に関係しそうなキー群がかなり大量にある ことが分かります。

次に、bubbleId の実例を1件だけ取るとこうなります。

SELECT key
FROM cursorDiskKV
WHERE key LIKE 'bubbleId:%'
ORDER BY key
LIMIT 1;
bubbleId:00a0cb91-168b-41e8-a043-b451407f7bc8:0cf6b529-d77d-4c05-aab1-717da4ad40c2

さらに value を文字列として見ると、先頭は次のような JSON でした。

SELECT CAST(value AS TEXT)
FROM cursorDiskKV
WHERE key LIKE 'bubbleId:%'
ORDER BY key
LIMIT 1;
{"_v":3,"type":2,"approximateLintErrors":[],"lints":[],"codebaseContextChunks":[],"commits":[],"pullRequests":[],"attachedCodeChunks":[],"assistantSuggestedDiffs":[],"gitDiffs":[],"interpreterResults":[],"images":[],"attachedFolders":[],"attachedFoldersNew":[],"bubbleId":"0cf6b529-d77d-4c05-aab1-717da4ad40c2", ...}

ここから分かるのは、DB ファイルの場所が分かり、SQL を投げられるだけで、アプリ内部がどんな粒度で状態を保存しているかをかなり覗ける ということです。
しかも、ここでは本文そのものより先に、lintcontextdifftoolResults のような 周辺状態の構造 が見えてきます。


3. SQLiteの中を調べると何が見えるか

globalStorage\state.vscdb の cursorDiskKV を確認すると、会話まわりに見える次のようなキーが見つかりました。

  • bubbleId:*
  • composerData:*
  • messageRequestContext:*

3-1. 確認できたこと

特に bubbleId:* は数が多く、調査時点のこの環境では 11,518 件 ありました。
composerData:* は 73 件messageRequestContext:* は 820 件 でした。

つまり、cursorDiskKV の中には会話まわりの状態が断片的に少しあるのではなく、かなりの規模で保存されている と見てよさそうです。

また、bubbleId:* の値の一部は JSON として読めました。

見えた範囲では、ペイロードの中に次のような項目が含まれていました。

  • bubbleId
  • codebaseContextChunks
  • attachedCodeChunks
  • diffHistories
  • toolResults
  • docsReferences
  • webReferences
  • images

このことから、bubbleId の中身は単純な「会話本文」ではなく、会話の表示状態、参照情報、差分、添付情報などをまとめた独自構造 である可能性が高そうです。

3-2. まだ断定できないこと

今回の簡易解析では、role content messages のような 人がそのまま読める本文の形 は見つかりませんでした。

ただし、ここで「SQLiteには本文がない」と断定するのは早いです。考えられる可能性としては、例えば次のようなものがあります。

  • 独自エンコードされている
  • 圧縮されている
  • 差分参照の形になっている
  • 別の構造から復元される前提になっている

したがって、現時点では 「SQLiteに会話関連データはあるが、本文が見えやすい形では確認できなかった」 と表現するのが一番正確です。


4. チャット本文は agent-transcripts に平文であった

一方で、チャット本文そのものに近いテキストは、別の場所で確認できました。

  • %USERPROFILE%\.cursor\projects\{projectSlug}\agent-transcripts\*.jsonl

この jsonl を見ると、各行が例えば次のような形になっています。

{"role":"user","message":{"content":[{"type":"text","text":"..."}]}}
{"role":"assistant","message":{"content":[{"type":"text","text":"..."}]}}

つまり、テキストとしての会話内容を追いたいなら、まず transcript の方が読みやすい ということです。

4-1. transcript が強い点

  • 平文テキストとして検索しやすい
  • バックアップしてあとで読み返しやすい
  • 会話ログの保管先として扱いやすい

4-2. transcript だけでは言い切れない点

  • Cursor の UI 上の一覧と、どこまで1対1で対応しているかは未確定
  • state.vscdb 側の状態情報とどう結びついているかは追加調査が必要

そのため、「本文の保全」と「UI上の復元」は別問題 として考えた方が整理しやすいです。


5. Cursorのチャットをバックアップするなら何を保存するか

ここまでを踏まえると、何をバックアップすべきかは目的によって変わります。

5-1. 会話テキストを失いたくないなら

次の4つを候補として押さえておくと安心です。

  • globalStorage\state.vscdb
  • state.vscdb-wal
  • state.vscdb-shm
  • agent-transcripts

特に agent-transcripts は本文参照に強く、state.vscdb は Cursor の内部状態全体に関係していそうです。
どちらか片方だけで十分とは言い切れないため、実務的には両方を持つ方が安全 です。

5-2. なぜ -wal と -shm も意識するのか

SQLite は WAL モードで動くことがあり、その場合は 最新の更新が本体ファイルではなく -wal 側にたまっている ことがあります。

そのため、

  • 本体だけコピーすると最新状態が欠けるかもしれない
  • 別時点の state.vscdb と state.vscdb-wal を混ぜると整合性を崩すかもしれない

という注意があります。

バックアップを取るなら、Cursor を終了したあとに、同じタイミングのスナップショットとしてまとめて保存する のが安全です。


6. 結論:Cursorの保存を理解するとSQLiteの実用例がよく分かる

SQLite というと、学習用の小さなデータベースやブラウザ内SQL実行環境を思い浮かべる人も多いと思います。
しかし実際には、Cursor のような日常的に使う開発ツールの内部でも、ローカル状態の保存先として SQLite が使われている と考えると、その実用性がかなり具体的に見えてきます。

今回の調査から整理すると、次のように言えます。

  • state.vscdb には会話に関係する状態データが入っていそう
  • ただし本文が平文でそのまま見えるとは限らない
  • 本文テキストを見るなら agent-transcripts が分かりやすい
  • バックアップでは SQLite 本体だけでなく -wal や transcript も重要になる

「検索したい」のか、「あとで読み返したい」のか、「UI上で復元したい」のかで、見るべき保存先は少しずつ変わります。
この違いを押さえておくと、Cursor のデータ管理も、SQLite の使われ方も理解しやすくなります。


未確定事項として残っている点

最後に、この調査にはまだ未確定な部分もあります。

  • bubbleId ペイロードのどこが最終的な表示本文に当たるのか
  • conversationState のような値がどう復元されているのか
  • transcript と SQLite 側のデータをどう対応付けられるのか

このあたりは、今後さらに深掘りできる技術メモのテーマ です。
必要なら次の記事で、「実際に SQLite からどこまで復元できるか」を検証編として切り出しても面白そうです。


次に読む

タイトルとURLをコピーしました