Claude Code の並行セッション問題を解決する:WAL モード対応 Memory MCP の実装
はじめに
Claude Code で複数セッションを並行実行してたら、こんなエラーに遭遇しませんでした?
Error: database is locked
正直、最初見たときは「え、まじで?」って思いました。Memory MCP は Claude Code のセッション間で知識を共有する強力なツールなんですが、公式実装が並行アクセスに非対応だったんですよね。
で、SQLite の WAL モードを使って並行セッション問題を根本から解決した Memory MCP を作ってみました。この記事ではその実装を紹介します。
対象読者
- Claude Code を日常的に使ってる開発者
- MCP(Model Context Protocol)に興味がある方
- 複数の AI セッションを並行実行したい方
- SQLite の実践的な活用方法を学びたい方
公式 Memory MCP の課題
公式の Memory MCP(@modelcontextprotocol/server-memory)は、知識グラフを JSONL ファイルで管理してます。これが結構厳しい。
1. 並行アクセスに弱い
// 公式実装のイメージ
const data = await fs.readFile('memory.jsonl', 'utf-8');
// ... 処理 ...
await fs.writeFile('memory.jsonl', newData);
ファイルロック機構がないんで、複数セッションが同時に書き込むとデータが壊れます。普通に怖い。
2. 検索パフォーマンスの限界
全データを読み込んでから線形検索するんで、データ量が増えるとどんどん遅くなる。あるあるですね。
3. トランザクション非対応
部分的な更新が失敗しても、データの整合性が保証されない。これはさすがにまずい。
解決策:SQLite + WAL モード
というわけで、SQLite ベースの Memory MCP を実装しました。
WAL(Write-Ahead Logging)モードとは
SQLite の WAL モード、これが本当に便利で:
- 並行読み取り・書き込みが可能:複数のリーダーと1つのライターが同時にアクセスできる
- クラッシュリカバリー:書き込み途中で異常終了してもデータ整合性を保証
- 高速な書き込み:ログベースなんで従来のロールバックジャーナルより速い
地味に革命的な機能です。
実装の詳細
データベース初期化
import Database from 'better-sqlite3';
import path from 'path';
import fs from 'fs';
export class KnowledgeGraphStore {
private db: Database.Database;
constructor(dbPath: string) {
// ディレクトリが存在しない場合は作成
const dir = path.dirname(dbPath);
if (dir && dir !== '.') {
fs.mkdirSync(dir, { recursive: true });
}
this.db = new Database(dbPath);
// WALモード有効化:並行アクセスを可能に
this.db.pragma('journal_mode = WAL');
// ビジータイムアウト設定:ロック時に5秒待機
this.db.pragma('busy_timeout = 5000');
this.initSchema();
}
}
ポイント:
-
journal_mode = WAL:並行読み書きを有効化 -
busy_timeout = 5000:ロック競合時に5秒待機してリトライ
busy_timeout を設定しとかないと、即座にエラーになっちゃうんで必須です。
スキーマ設計
知識グラフを効率的に管理するため、3つのテーブルで構成します:
private initSchema(): void {
this.db.exec(`
-- エンティティ(概念)を管理
CREATE TABLE IF NOT EXISTS entities (
name TEXT PRIMARY KEY,
entity_type TEXT NOT NULL
);
-- エンティティに関する観察・事実を記録
CREATE TABLE IF NOT EXISTS observations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
entity_name TEXT NOT NULL,
content TEXT NOT NULL,
FOREIGN KEY (entity_name) REFERENCES entities(name) ON DELETE CASCADE,
UNIQUE(entity_name, content) -- 重複防止
);
-- エンティティ間の関係を管理
CREATE TABLE IF NOT EXISTS relations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_entity TEXT NOT NULL,
to_entity TEXT NOT NULL,
relation_type TEXT NOT NULL,
FOREIGN KEY (from_entity) REFERENCES entities(name) ON DELETE CASCADE,
FOREIGN KEY (to_entity) REFERENCES entities(name) ON DELETE CASCADE,
UNIQUE(from_entity, to_entity, relation_type) -- 重複防止
);
-- パフォーマンス最適化用インデックス
CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_name);
CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity);
CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity);
`);
}
設計のポイント:
-
CASCADE:エンティティ削除時に関連データも自動削除(孤立データを防ぐ) -
UNIQUE制約:重複データの防止 - インデックス:検索パフォーマンスの向上
インデックスは地味に重要。これがないと検索が遅くてストレス溜まります。
トランザクション対応の更新処理
createEntities(entities: Entity[]): Entity[] {
const insertEntity = this.db.prepare(
'INSERT OR IGNORE INTO entities (name, entity_type) VALUES (?, ?)'
);
const insertObservation = this.db.prepare(
'INSERT OR IGNORE INTO observations (entity_name, content) VALUES (?, ?)'
);
const created: Entity[] = [];
// トランザクションで一括処理
const transaction = this.db.transaction((entities: Entity[]) => {
for (const entity of entities) {
insertEntity.run(entity.name, entity.entityType);
for (const obs of entity.observations) {
insertObservation.run(entity.name, obs);
}
created.push(entity);
}
});
transaction(entities);
return created;
}
トランザクションの利点:
- 原子性:途中で失敗しても部分的な変更は残らない
- 整合性:複数テーブルへの更新が一貫性を保つ
- パフォーマンス:一括コミットで高速化
トランザクション使わないと、エラー時に中途半端なデータが残って悲惨なことになります。経験済み。
パッケージとして公開
npm パッケージとして公開したんで、誰でも簡単に使えます。
インストール
npm install @pepk/mcp-memory-sqlite
Claude Desktop / Claude Code での設定
~/.claude.json(またはグローバル設定)に以下を追加:
{
"mcpServers": {
"memory": {
"command": "npx",
"args": ["@pepk/mcp-memory-sqlite"],
"env": {
"MEMORY_DB_PATH": "./.claude/memory.db"
}
}
}
}
設定のポイント:
-
MEMORY_DB_PATH:プロジェクトごとに./.claude/memory.dbを推奨 - グローバル共有も可能:
~/memory.dbなどに設定
プロジェクトごとに分けた方が管理しやすいです。経験上。
公式実装との比較
| 項目 | 公式(JSONL) | 自作(SQLite + WAL) |
|---|---|---|
| 並行アクセス | ❌ 非対応 | ✅ 対応(WAL) |
| トランザクション | ❌ なし | ✅ ACID 保証 |
| 検索速度 | 遅い(線形検索) | 速い(インデックス) |
| データ整合性 | 弱い | 強い(外部キー制約) |
| クラッシュリカバリー | ❌ 手動修復 | ✅ 自動(WAL) |
表で見ると、結構違いますね。
トラブルシューティング
エラー:database is locked
WAL モードでも発生する場合:
// busy_timeout を延長
this.db.pragma('busy_timeout = 10000'); // 10秒
5秒で足りなければ10秒にしてみてください。
WAL ファイルが肥大化
# チェックポイント実行
sqlite3 memory.db "PRAGMA wal_checkpoint(TRUNCATE);"
定期的に実行すると WAL ファイルのサイズが抑えられます。
まとめ
SQLite の WAL モードを活用して、並行セッション問題を解決した Memory MCP を実装しました。
主なポイント:
- WAL モードで並行読み書きを実現
- トランザクションでデータ整合性を保証
- インデックスで検索パフォーマンスを向上
- npm パッケージとして公開済み
個人的には、WAL モードの並行アクセス対応が一番のポイントかなと。これで複数セッション開いても安心です。
リソース
- npm パッケージ: @pepk/mcp-memory-sqlite
- GitHub リポジトリ: Daichi-Kudo/mcp-memory-sqlite
- SQLite WAL モード: SQLite Write-Ahead Logging
他のプラットフォームで読む
- Zenn: 同じ記事をZennで読む
- note(体験談): 開発の裏話・ストーリー版
- dev.to(English): Read in English
この実装が Claude Code の並行セッション問題に悩む方の助けになれば嬉しいです。Issue や PR もお待ちしてます!
著者について
Daichi Kudo
- Cognisant合同会社 CEO - 人とAIが共に創る未来を目指し、AI開発支援
- M16合同会社 CTO - AI・クリエイティブ・エンジニアリング