0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code の並行セッション問題を解決する:WAL モード対応 Memory MCP の実装

Last updated at Posted at 2026-01-01

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 モードの並行アクセス対応が一番のポイントかなと。これで複数セッション開いても安心です。

リソース

他のプラットフォームで読む


この実装が Claude Code の並行セッション問題に悩む方の助けになれば嬉しいです。Issue や PR もお待ちしてます!

著者について

Daichi Kudo

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?