バグトラッカー
このドキュメントは、ErisPulse SDK の既知のバグとその修正状況を記録したものです。修正されたバージョンの日付順に並べられています。
読者の皆様へ どんなソフトウェアにも完璧さは存在せず、どんなに注意深く開発しても小さな間違いが残ります。本トラッカーに記録されているのは、実行に実際に影響を与える問題のみです。あまりにも些細で「軽度」のレベルにも達しないような欠陥は、ここには含まれません。リストに「重大」の項目が多めに見えるかもしれませんが、公開されている目的は、トラブルシューティングと再現をスムーズにすることであり、不安を煽ることではありません。目にする、記録する、修正する問題こそが、プロジェクトが常に良くなっている証拠です。このリストを見て不安になる必要はありません。これはトラブルシューティングのためのツールであり、恐怖の原因ではありません。
読み方とメンテナンスの規約
- 各バグの記録には、問題の説明、根本原因分析、影響を受けたバージョンの範囲、修正方法などの構造化されたフィールドが含まれています。アップグレードする前に、「影響バージョン」が現在使用しているバージョンをカバーしているか確認することを推奨します。
- 新しいバグの条目を追加する場合は、対応する位置に内容を追加し、以下のフィールド規格と重大度/タイプの分類に従ってください。
フィールドの説明
必須フィールド
| フィールド | 説明 |
|---|---|
| 問題 | バグの外的な現象、ユーザーが観察できる異常現象。エラーメッセージや典型的なシナリオをできるだけ記述してください |
| 原因 | 具体的なコードの欠陥を指す根本原因分析(複雑な場合に「根本原因の経路」図示) |
| 影響バージョン | 影響を受けたバージョンの範囲、形式 導入バージョン - 修正バージョン(両端の dev バージョンを含む) |
| 修正バージョン | このバグを修正した具体的なバージョン番号 |
| 修正内容 | 修正方法の簡潔な説明、重要なコード変更点を含む |
| 修正日 | 対応する修正バージョンのリリース日、YYYY/MM/DD 形式 |
| 重大度 | 下記「重大度の分類」に従って記載 |
| タイプ | 下記「タイプの分類」に従って記載、複数選択可能(例: アダプタ / ルーティング) |
任意フィールド
| フィールド | 説明 | 適用場面 |
|---|---|---|
| 再現手順 | このバグを引き起こす最小限の再現手順 | 複雑なバグ、偶発性バグには推奨 |
| 関連 | 関連する Issue / PR / Commit リンク | 外部の議論記録がある場合に追加 |
| リグレッションテスト | 修正を検証し、再発を防ぐためのテストケースの位置 | pytest 用のテストケースが書かれている場合に追加 |
重大度の分類
| 記号 | 級別 | 判定基準 | 典型的な表現 |
|---|---|---|---|
| 🔴 | 重大 | プロセスのクラッシュ、データの損失/破損、コア機能の完全不可用、セキュリティの脆弱性 | OOM Kill、メッセージの送信不可、モジュールのロード不可、ホットリロードの失敗 |
| 🟡 | 中等 | 機能の異常だが回避方法がある、コア機能以外の機能が失敗、偶発的な問題 | 状態の判断の誤り、重複実行、キャッシュの期限切れ、エラーメッセージの不正確 |
| 🟢 | 軽度 | コア機能に影響しない、コード品質や体験の問題、潜在的なリスクが未発生 | 非推奨のAPI、死んだコード、警告ログの欠如 |
タイプの分類
| タイプ | 覆う範囲 |
|---|---|
| 設定システム | ConfigManager、設定の読み書き、設定のSchema、ホット更新 |
| イベントシステム | Event モジュール(command/message/notice/request/meta)、イベントの配信、ハンドラの登録 |
| アダプタ | AdapterManager、BaseAdapter、アカウントの解析、Botの状態、ミドルウェア |
| ルーティング | RouterManager、HTTP/WebSocket/SSEルーティング、リクエスト制限、CORS |
| クライアント | HttpClient、ClientWebSocket、aiohttpのラッパー |
| ストレージ | StorageManager、SQLite、SQLビルダー、ネストされたキー |
| 加載システム | Loader、LazyModule、ModuleInitializer、厳密モード、モジュールの発見 |
| CLI | epsdk コマンド、init/run/install、パラメータの解析、シグナルの処理 |
| 実行時 | sdk.run/restart/uninit、ライフサイクル、シグナル、サブプロセス |
条目テンプレート
新しいバグの条目は以下のフォーマットに従ってください:
### [BUG-XXX] タイトル
**問題**: 問題の説明(エラーメッセージや典型的な現象)
**原因**: 根本原因分析
**影響バージョン**: 導入バージョン - 修正バージョン
**修正バージョン**: x.x.x
**修正内容**: 修正方法
**修正日**: YYYY/MM/DD
<!-- 任意フィールド -->
**再現手順**: (複雑なバグには推奨)
**関連**: (Issue/PR リンク)
**リグレッションテスト**: (検証用のテストケースのパス)
**重大度**: 🔴 重大 | 🟡 中等 | 🟢 軽度
**タイプ**: 設定システム / イベントシステム / アダプタ / ルーティング / クライアント / ストレージ / 加載システム / CLI / 実行時
統計概要
| 重大度 | 数量 |
|---|---|
| 🔴 重大 | 16 |
| 🟡 中等 | 18 |
| 🟢 軽度 | 3 |
| 合計 | 37 |
| タイプ | 数量 |
|---|---|
| アダプタ | 6 |
| 設定システム | 11 |
| イベントシステム | 7 |
| CLI | 3 |
| ストレージ | 3 |
| 加載システム | 3 |
| ルーティング | 2 |
| クライアント | 1 |
| 実行時 | 1 |
注:1つのバグは複数のタイプに属する可能性がある。上表は主タイプでカウントしている。
注:BUG-028 / BUG-031の番号は空いている(登録時に廃棄され、再利用されないよう安定した番号の維持のため)。
修正済みのバグ
[BUG-001] イベントハンドラが重複登録され、イベントが複数回処理される
問題: 複数の @message / @notice などのデコレータを使ってハンドラを登録すると、同じイベントが複数回トリガーされ、コマンドが複数回実行され、ログが重複出力される。
原因: BaseEventHandler がアダプタのイベントバスにハンドラを登録する際に重複除去のロジックがなく、各デコレータが一度ずつバスに登録され、イベント配信時に複数回呼び出される。
影響バージョン: 2.2.0-dev.0 - 2.2.1-dev.0
修正バージョン: 2.2.1-dev.0
修正内容: BaseEventHandler を最適化し、各イベントタイプがアダプタに一度だけハンドラを登録するようにし、重複実行を防ぐ。
修正日: 2025/08/18
重大度: 🔴 重大
タイプ: イベントシステム
[BUG-002] Init コマンドのアダプタ設定パスの型エラー
問題: ep init コマンドを使ってインタラクティブに初期化する際に、アダプタの設定を選択すると型エラーが発生する:
インタラクティブな初期化が失敗しました: 'str' と 'str' の / 演算子はサポートされていません
原因: 2.3.7 バージョンで設定ファイルのパスを調整した際、メソッドのパラメータ型が一貫しておらず、_configure_adapters_interactive_sync は str 型のパラメータを受け取るが、内部で Path の / 演算子を使ってパスを結合している。
影響バージョン: 2.3.7 - 2.3.9-dev.1
修正バージョン: 2.3.9-dev.1
修正内容: _configure_adapters_interactive_sync メソッドのパラメータ型を str から Path に変更し、呼び出し時に Path オブジェクトを直接渡す。
修正日: 2026/03/23
重大度: 🟡 中等
タイプ: CLI
[BUG-003] 再起動後にコマンドイベントが無効になる
問題: sdk.restart() を呼び出すと、@command で登録されたコマンドがトリガーされなくなり、送信されたコマンドに対してロボットが応答しなくなる。
原因: adapter.shutdown() でイベントバスをクリアした後、BaseEventHandler の _linked_to_adapter_bus 状態が False にリセットされていないため、_process_event メソッドがすでにアダプタのバスに接続されていると判断し、再接続処理をスキップする。
影響バージョン: 2.2.x - 2.4.0-dev.2
修正バージョン: 2.4.0-dev.3
修正内容: _linked_to_adapter_bus 状態を追跡する _clear_handlers() を導入し、shutdown/restart の状況に適応するように、次回の register() で自動的に再接続する。
修正日: 2026/04/09
重大度: 🔴 重大
タイプ: イベントシステム
[BUG-004] ライフサイクルイベントハンドラがクリアされない
問題: sdk.restart() の後、古いライフサイクルイベントハンドラが存在し、同じイベントが複数回処理されるため、イベントが複数回実行される。
原因: lifecycle._handlers 辞書は uninit() 時にクリアされておらず、restart 後に古いハンドラと新しいハンドラが共存する。
影響バージョン: 2.3.0 - 2.4.0-dev.2
修正バージョン: 2.4.0-dev.3
修正内容: Uninitializer のクリーンアップフローの末尾(すべてのイベントが送信された後)、lifecycle._handlers をクリアする。
修正日: 2026/04/09
重大度: 🟡 中等
タイプ: 実行時
[BUG-005] Event.is_friend_add/is_friend_delete の detail_type が OB12 標準と一致しない
問題: Event.is_friend_add() は detail_type == "friend_add" をチェックし、Event.is_friend_delete() は detail_type == "friend_delete" をチェックするが、OneBot12 標準では detail_type の値は "friend_increase" と "friend_decrease" である。notice.py の on_friend_add/on_friend_remove デコレータが使用する値と一致しないため、デコレータで登録されたハンドラがトリガーされたときに、対応する is_friend_add()/is_friend_delete() 判断メソッドが False を返す。
原因: wrapper.py で非標準の命名が使用されており、notice.py では正しい OB12 標準の命名が使用されている。
影響バージョン: 実装以来
修正バージョン: 2.4.2-dev.1
修正内容: is_friend_add() のマッチング値を "friend_add" から "friend_increase" に、is_friend_delete() を "friend_delete" から "friend_decrease" に変更する。
修正日: 2026/04/13
重大度: 🟡 中等
タイプ: イベントシステム
[BUG-006] adapter.clear() が _started_instances をクリアしないため、再起動後に状態が正しくない
問題: AdapterManager.clear() メソッドは _adapters、_adapter_info、ハンドラ、_bots をクリアするが、_started_instances 集合をクリアしていない。アダプタが実行中に clear() を呼び出すと、_started_instances は孤立した参照を保持し、再起動後に状態の判断が誤る。
原因: 2.4.0-dev.1 で _started_instances を導入した際に、clear() で同期してクリアしていない。
影響バージョン: 2.4.0-dev.1 - 2.4.2-dev.0
修正バージョン: 2.4.2-dev.1
修正内容: clear() メソッドに self._started_instances.clear() を追加する。
修正日: 2026/04/13
重大度: 🟡 中等
タイプ: アダプタ
[BUG-007] command.wait_reply() で廃止された asyncio.get_event_loop() を使用している
問題: CommandHandler.wait_reply() メソッドは asyncio.get_event_loop() を使って future を作成し、タイムスタンプを取得しているが、このメソッドは Python 3.10+ で廃止されており、非同期コンテキストでは asyncio.get_running_loop() を使用する必要がある。wrapper.py の wait_for() メソッドで使用されている get_running_loop() と不一致である。
原因: 開発時に古い API を使用しており、後で追加された wait_for() は正しい API を使用しているが、古いコードを後回しに修正していない。
影響バージョン: 2.3.0-dev.0
修正バージョン: 2.4.2-dev.1
修正内容: command.py の asyncio.get_event_loop() を asyncio.get_running_loop() に置き換える。
修正日: 2026/04/13
重大度: 🟢 軽度
タイプ: イベントシステム
[BUG-008] Bot オンラインイベントが shutdown 過程中に重複送信される
問題: adapter.shutdown() を呼び出してすべてのアダプタを閉じる際に、_update_bot_status() が閉じるプロセス中に Bot オンラインイベントを繰り返し送信し、同一批の Bot が複数回オフラインとマークされ、adapter.bot.offline ライフサイクルイベントが複数回トリガーされる。
原因: 2.4.0-dev.1 で導入された Bot 状態追跡システムは、shutdown() 間の「閉じ中」フラグを設定しておらず、_update_bot_status() が通常のオフラインと閉じるプロセス中の連鎖オフラインを区別できない。
影響バージョン: 2.4.0-dev.1 - 2.4.2-dev.1
修正バージョン: 2.4.2-dev.1
修正内容: AdapterManager に _is_being_shutdown フラグを追加し、shutdown() 開始時に True に設定し、終了時にクリアする。_update_bot_status() はこのフラグをチェックして、閉じるプロセス中の重複送信をスキップする。
修正日: 2026/04/21
重大度: 🟡 中等
タイプ: アダプタ
[BUG-009] LazyModule が同期アクセスで BaseModule を初期化していない
問題: 同期コンテキストで LazyModule の属性にアクセスする際に、loop.create_task() で非同期初期化を行うが、待機しないため、初期化が完了していない状態で属性にアクセスされ、競合状態が発生する。
原因: _ensure_initialized() が loop.create_task(self._initialize()) の後に即座に返り、初期化が完了していない。
影響バージョン: 2.4.0-dev.0 - 2.4.2-dev.1
修正バージョン: 2.4.2-dev.2
修正内容: 同期コンテキストでは、BaseModule の初期化を asyncio.run(self._initialize()) に変更し、初期化が完了してから返す。透明プロキシの特性を維持し、ユーザーは同期/非同期の違いを意識する必要がない。
修正日: 2026/04/21
重大度: 🟡 中等
タイプ: 加載システム
[BUG-010] 複数スレッドでの書き込みで設定データが失われる
問題: 多スレッド環境で複数のスレッドが config.setConfig() を同時に呼び出すと、_flush_config() は読み取り-変更-書き込み操作をアトミックにしないため、一部の書き込みが失われる可能性がある。
原因: _flush_config() は RLock を使用しているが、ファイルの読み取りと書き込みの間にはファイルロックの保護がなく、_schedule_write のタイマーが複数回トリガーされて上書きされる可能性がある。
影響バージョン: 2.3.0 - 2.4.2-dev.1
修正バージョン: 2.4.2-dev.2
修正内容:
- ファイルロックメカニズム(
_file_lock)を追加してファイル操作をアトミックにする - 一時ファイルに書き込んだ後、
os.replace/os.renameでアトミックにリネームする _schedule_writeのタイマーのキャンセルと再スケジュールのロジックを改善する
修正日: 2026/04/21
重大度: 🔴 重大
タイプ: 設定システム
[BUG-011] Windows で CTRL+C でプログラムを停止できない
問題: Windows で python main.py を直接実行した後、CTRL+C を押すとプログラムが停止せず、プログラムは正常に起動してルーティングサーバーの情報を出力した後、CTRL+C は全く反応せず、タスクマネージャーで強制的にプロセスを終了するしかない。一方、epsdk run で起動した場合は正常に停止できるが、epsdk run はサブプロセスモデルで実行される。
原因: Hypercorn ASGI サーバーの serve() 関数内部で signal.signal(SIGINT, handler) を使って独自の SIGINT ハンドラを登録しており、Python のデフォルトの KeyboardInterrupt ハンドラを上書きしている。asyncio.create_task() でバックグラウンドタスクとして Hypercorn を起動した場合、Hypercorn の内部のシャットダウンフローが正しくトリガーされない(worker_serve モードを想定しているため)、CTRL+C シグナルは Hypercorn に飲み込まれて、クリーンアップアクションを引き起こさない。
影響バージョン: 2.3.6 - 2.4.2
修正バージョン: 2.4.3-dev.0
修正内容:
- ASGI サーバーを Hypercorn から Uvicorn に切り替える(
pyproject.toml依存関係の変更) uvicorn.Server._serve()を直接呼び出してcapture_signals()シグナル処理コンテキストマネージャーを回避するserver.should_exit = Trueを使って優雅な停止を実現し、タイムアウトしたらバックグラウンドタスクをキャンセルする- サブプロセス実行モデルと
runtime/cleanup.pyクリーンアップモジュールを同期的に削除する(サブプロセスのクリーンアップ機構は不要)
修正日: 2026/04/28
重大度: 🔴 重大
タイプ: CLI / 実行時
[BUG-012] ホットリスタート後に更新されたモジュールの Python コードが効果がない
問題: sdk.restart() でソフトリスタートした後、epsdk install でアップグレードされたモジュール/アダプタの新しいコード(追加された API ルートなど)が効果せず、以前のロジックが実行される。完全にプロセスを再起動するまで最新のコードがロードされない。
原因: _do_restart() が再初期化時に entry_point.load() を呼び出すが、この関数は sys.modules からキャッシュされた古いモジュールオブジェクトを返し、ディスクから再読み込みしない。
影響バージョン: 早期バージョン - 2.4.3-dev.1
修正バージョン: 2.4.3-dev.1
修正内容: uninit() 後、init() 前に sys.modules にロードされたモジュール/アダプタパッケージのキャッシュをクリアし、entry_point.load() が最新のコードをロードできるようにする。_collect_top_level_modules() と _invalidate_module_cache() 補助メソッドを追加し、top_level.txt または entry-point value からトップレベルモジュール名を推論する。
修正日: 2026/05/03
重大度: 🔴 重大
タイプ: 加載システム / 実行時
[BUG-013] モジュールのロード戦略のソートロジックが間違っている
問題: ModuleLoadStrategy は priority フィールドを提供してモジュールの初期化優先度を宣言するが、ロード戦略の実装にミスがあり、モジュールが期待通りの優先順位で初期化されず、entry_points() のデフォルト順序でロードされる。モジュール間で依存関係がある場合、priority を使って正しい初期化順序を保証できない。
原因: ロード戦略の実装でソートロジックが間違っている。initialize_modules() は priority を使ってモジュールリストをソートしていない。
影響バージョン: 2.3.4 - 2.4.5-dev.2
修正バージョン: 2.4.5-dev.3
修正内容: initialize_modules() のループ前に、priority を降順でソートする。同じ priority のモジュールは元の相対順序を保持する(安定ソート)。
修正日: 2026/05/15
重大度: 🟡 中等
タイプ: 加載システム
[BUG-014] アダプタミドルウェアが None を返すとイベントデータが失われる
問題: adapter.emit() が OneBot12 ミドルウェアチェーンを実行する際、あるミドルウェアが None を返す(例:return data を忘れている)と、後続のミドルウェアとすべてのイベントハンドラが processed_data が None になるため、イベント処理が完全に無効になる。
原因: ミドルウェアチェーンの実装 processed_data = await middleware(processed_data) は、戻り値が None かどうかをチェックせず、上記の処理結果を上書きする。
影響バージョン: unknown - 2.4.5-dev.3
修正バージョン: 2.4.5-dev.4
修正内容: ミドルウェアが None を返した場合、その返り値を無視して元のデータを保持し、警告レベルのログを出力する。
修正日: 2026/05/15
重大度: 🔴 重大
タイプ: アダプタ / イベントシステム
[BUG-015] 設定ファイルのパスが作業ディレクトリに依存する
問題: ConfigManager の設定ファイルのパスはデフォルトで相対パス "config/config.toml" であり、実行時に os.getcwd() で解析される。作業ディレクトリが実行中に変更された場合(例:os.chdir() を使用する)、設定ファイルの読み書き操作が間違った場所を指し、設定が失われたり、古いデータが読み込まれたりする。
原因: __init__ で相対パスを直接保存しており、初期化時に絶対パスに解析していない。
影響バージョン: 2.3.7 - 2.4.5-dev.3
修正バージョン: 2.4.5-dev.4
修正内容: ConfigManager.__init__() で、渡されたパスが相対パスの場合は、os.path.abspath() で絶対パスに解析する。
修正日: 2026/05/15
重大度: 🟡 中等
タイプ: 設定システム
[BUG-016] BaseStorage はストレージ値が None とキーが存在しないことを混同する
問題: BaseStorage.get_multi() / __getattr__() は「キーが存在しない」と「キーの値が None」の2つの状況を区別できず、ユーザーが明示的に None を保存した後に読み取ると、キーが存在しないとみなされる。
原因: 取り出しロジックで value is None でキーの存在を判断しており、独立した「欠損」マーカーがない。
影響バージョン: 早期バージョン - 2.4.6-dev.6
修正バージョン: 2.4.6-dev.6
修正内容: _SENTINEL センティネル値を導入して「キーが存在しない」と「値が None」を区別し、2つを混同しないようにする。
修正日: 2026/06/07
重大度: 🟡 中等
タイプ: ストレージ
[BUG-017] WebSocketルートの auto_accept フラグがサービス再起動後に失われる
問題: サービスが再起動(例:sdk.restart())した後、すべてのWebSocketルートの auto_accept 設定が False に戻り、元々 auto_accept が有効な接続は保留状態になり、クライアントは長時間レスポンスを受け取れず、WebSocket接続がフリーズする。
原因: _restore_routes_from_records() が永続化記録からルートを復元する際に auto_accept を False にハードコードしており、元の記録から値を読まない。また、ルートストアのタプルが2タプルから3タプルに拡張された際に、復元ロジックも同期して更新されていない。
影響バージョン: 2.3.8-dev.0 - 2.4.6-dev.6
修正バージョン: 2.4.6-dev.6
修正内容: ルートストアのタプルを (handler, auth_handler, auto_accept) に拡張し、_restore_routes_from_records() は記録から本当の auto_accept 値を読み取るようになり、False をハードコードしない。
修正日: 2026/06/07
重大度: 🔴 重大
タイプ: ルーティング
[BUG-018] HTTP/WSクライアントの並行呼び出しによるクラッシュと接続リーク
問題: Core/client.py のHTTPおよびWebSocketクライアントは、並行処理の際に複数の安定性の問題があり、接続のリークやプロセスのクラッシュを引き起こす可能性がある:
- 多コルーチンで
ClientWebSocket.receive()を並行呼び出すと、aiohttpがConcurrent call to receive() is not allowedを投げる _get_http_session()/_get_ws_session()の並行呼び出しは複数のセッションを作成し、_drain_sessions()が古い接続を閉じないため、接続のリークが発生するrequest()の例外捕獲の順序が間違っている:except ClientConnectionError(ErisPulseの例外)は決して発生せず、aiohttpの接続エラーは一般的なexcept Exceptionに捕獲され、"接続リトライ + セッション再作成"のロジック(死コード)は決して実行されないsend_json()はmode="binary"パラメータを無視する;_get_ws_session()にデフォルトのリクエストヘッダーを渡さない
原因: クライアントの初期実装(2.4.6-dev.5)には並行処理の保護と、aiohttpの例外体系とErisPulse独自例外の継承関係の処理が不十分だった。
影響バージョン: 2.4.6-dev.5 - 2.4.8
修正バージョン: 2.4.8
修正内容:
_recv_lockを追加してreceive()/receive_text()/receive_bytes()の呼び出しをシーケンス化する_session_lockを追加してセッションの作成を保護する;_drain_sessions()を非同期メソッドに変更し、古いセッションを実際に閉じるrequest()の例外捕獲の順序を再構成する:asyncio.TimeoutError→aiohttp.ClientConnectionError(セッションの再作成をトリガー)→aiohttp.ClientError→ClientError(透過)→Exceptionsend_json()のmode処理、_get_ws_session()へのデフォルトリクエストヘッダーの渡し方、close()の並行競合、HttpResponse.__aexit__の重複release()
修正日: 2026/06/12
重大度: 🔴 重大
タイプ: クライアント
[BUG-019] アダプタのホットリロード時にルートの競合によりリロードに失敗する
問題: 第三者モジュール(例:Dashboard)がアダプタのホットリロードをトリガーするか、アダプタの起動に失敗してリトライする際に、以前に登録された古いルート(例:onebot11_default)がクリアされていないため、WebSocketパス ... は既に登録されていますの競合が発生し、リロードに失敗する。完全にプロセスを再起動するまで回復できない。
原因: AdapterManager.shutdown() は unregister_all_by_namespace(platform) でルートをクリアするが、アダプタ(例:OneBot11)は onebot11_{account_name} で命名空間としてWebSocketルートを登録しており、粒度が一致しないため、クリアが無効な操作になる。起動失敗リトライ時のルートも、前回の残りルートをクリアしていない。
影響バージョン: 早期バージョン - 2.4.9
修正バージョン: 2.4.9
修正内容:
- ルート登録時に
current_ownerContextVar を使ってowner → namespaceの所有関係を自動的に追跡する unregister_all_by_owner(owner)を追加し、停止/再起動時に所有者ごとにクリアする。これにより、細かい粒度の命名空間に対応する_stop_adapter(platform)という「停止即クリア」の原語を追加し、アダプタの停止とその登録されたリソースの回収を1回の呼び出しに結合する。restart()と起動失敗リトライはこのエントリーポイントを経由する- フレームワークレベルで
adapter.restart(platform)API を追加し、第三者モジュールはこのメソッドを呼び出すべきである
修正日: 2026/06/12
重大度: 🔴 重大
タイプ: アダプタ / ルーティング
[BUG-020] サブプロセスモード ep run <script> でスクリプトの子パッケージが見つからない
問題: ep r .\main.py で非ホットリロードモードでスクリプトを実行する場合、スクリプトに相対インポート(例:from qg import ...)がある場合、No module named 'qg' エラーが発生する。一方、--reload モードでは正常に実行できる。
原因: 非ホットリロードモードでは runpy.run_path() を直接呼び出してスクリプトを実行するため、スクリプトの所在ディレクトリを sys.path に自動的に追加しない。一方、--reload モードでは subprocess.Popen でサブプロセスを実行するため、サブプロセスは現在の作業ディレクトリを自動的に継承し、sys.path[0] がスクリプトの所在ディレクトリになるため、正常に動作する。
影響バージョン: 2.5.0 - 2.5.2-dev.0
修正バージョン: 2.5.2-dev.0
修正内容: runpy.run_path() を呼び出す前に、スクリプトの所在ディレクトリを sys.path[0] に手動で挿入する。
修正日: 2026/06/27
重大度: 🟡 中等
タイプ: CLI
[BUG-021] SQLクエリビルダーが合法的なワイルドカードとリスト式を拒否する
問題: SQLiteQueryBuilder の _build_select_sql() は、すべての SELECT 列に対して _validate_identifier() を呼び出すが、この関数は厳密なホワイトリスト正則 ^[a-zA-Z_][a-zA-Z0-9_]*$ を使用しており、合法な SQL 構文が不正な列名と誤判定される:
SELECT *—*は SQL 標準のワイルドカードSELECT COUNT(*)— 集計関数SELECT users.name— 限定列名SELECT col AS alias— 列のエイリアス
その中で Select("*") は Cron などのモジュールで使用され、モジュールの on_load 実行時に失敗し、モジュールがロードできない。
原因: 2.4.6 バージョンで SQL インジェクション対策が強化され、_validate_identifier() のホワイトリスト検証が導入された。この検証はすべての列名に適用されるが、読み取り側(SELECT/ORDER BY)と書き込み側(INSERT/UPDATE)を区別していない。SELECT 列には複雑な SQL 式が許容されるため、単純な識別子ホワイトリスト制限は不要である。
影響バージョン: 2.4.6 - 2.5.2-dev.1
修正バージョン: 2.5.2-dev.2
修正内容: SELECT/ORDER BY の列検証をホワイトリストモードからブラックリストモードに変更する:
_validate_select_column()関数を追加し、SQL インジェクションの危険な文字(;'"--/**/\x00改行)を検出する- 任意の合法な SQL 列式(
*、table.*、table.column、COUNT(*)、col AS aliasなど)を許容する - INSERT/UPDATE 列名は依然として厳密なホワイトリスト検証(単純な識別子のみ)
修正日: 2026/06/29
重大度: 🔴 重大
タイプ: ストレージ
[BUG-022] _resolve_account() アカウント解決のバグ(_accounts_data が埋め込まれていない)
問題: 2.5.2 設定システムの再構築後、AccountConfigClass を宣言したマルチアカウントアダプタで wait_reply、reply などのメッセージ送信メソッドを呼び出すと、ValueError("未宣言 AccountConfigClass、アカウントが解決できません") エラーが発生する。アダプタがマルチアカウント情報を正しく設定しても、アカウント解決は失敗する。
原因: 2.5.2-dev.5 で _load_accounts()(設定の読み取り + 検証 + _accounts_data の埋め込み)が _ensure_accounts_exist()(設定テンプレートの生成)に再構築されたが、_resolve_account() はまだ _accounts_data is None をチェックしている。_accounts_data は常に None のままなので、_resolve_account() は (None, None) を返し、アカウント解決が完全に失敗する。
根本原因の経路:
_load_accounts() が削除された
→ __init__ で _accounts_data を埋め込み直さない
→ _accounts_data は常に None である
→ _resolve_account() が _accounts_data is None をチェック → (None, None) を返す
→ 下流で _resolve_account を呼び出す (例: call_api) は None を受け取る
→ エラーが発生する
影響バージョン: 2.5.2-dev.5 - 2.5.2
修正バージョン: 2.5.3
修正内容: BaseAdapter.__init__ で、_ensure_accounts_exist() の後に _accounts_data の埋め込みを復元する:
if self.AccountConfigClass is not None:
self._ensure_accounts_exist()
self._accounts_data = self.accounts # 実際に読み取られた accounts 属性からデータソースを埋め込む
_resolve_account() のロジックは変更されず、完全に後方互換性がある:
AccountConfigClassを宣言していないアダプタ:_accounts_dataはNoneのまま →(None, None)を返すAccountConfigClassを宣言したアダプタ:_accounts_dataが埋め込まれる → 正常に解決_load_accountsを上書きまたは_accounts_dataを手動で設定したアダプタ:super().__init__()の後に上書きし、優先度が最も高い
修正日: 2026/07/07
重大度: 🔴 重大
タイプ: アダプタ / 設定システム
[BUG-023] アカウント設定を変更した後、アダプタのキャッシュが更新されずアカウント解決に失敗する
問題: ダッシュボードでマルチアカウントアダプタのアカウント設定(例:トークンの入力)を変更した後、アダプタは古いキャッシュを使用し、メッセージ送信関連のメソッドを呼び出すと 未使用アカウント (account_id=default) エラーが発生する。プロセスを再起動しないと新しい設定が有効になる。
原因: _accounts_data は BaseAdapter.__init__ 時に設定ストアから一度だけ読み取られ、以降は更新されない。AdapterManager._run_adapter() と restart() で adapter.start() を呼び出す前に、_accounts_data は再び設定ストアから読み取られず、キャッシュと実際の設定がずれる。
影響バージョン: 2.4.6 - 2.5.4
修正バージョン: 2.5.4
修正内容: AdapterManager._run_adapter() と restart() で、adapter.start() を呼び出す前に adapter._accounts_data = adapter.accounts を更新する。これで、各起動時に最新の設定を使用する。
修正日: 2026/07/09
重大度: 🔴 重大
タイプ: アダプタ / 設定システム
[BUG-024] storage.set() で大数 ID キーを書き込むと OOM Kill が発生する
問題: storage.set() で大数の純数フィールド(例:QQグループ番号 871684833)を含むネストされたキーを書き込むと、プロセスがコンテナ OOM Kill(終了コード -9)され、サービスがクラッシュして復旧できない。
原因: _set_nested_value の再帰実装で、ネストされたキーの純数フィールドが isdigit() によりリストインデックスと誤って判断され、current.extend([None] * (index - len(current) + 1)) を実行し、数億要素のリストを割り当てようとして、瞬時にメモリを消費し、メモリ不足 → コンテナ OOM Kill(終了コード -9)。
根本原因の経路:
キーのパスに純数フィールド(例:グループ番号 871684833)が含まれる
→ isdigit() が配列インデックスと誤って判断
→ extend([None] * (871684833 - len(current) + 1))
→ 数億要素のリストを割り当てようとする
→ メモリを消費し、コンテナ OOM Kill(終了コード -9)
影響バージョン: 2.5.1 - 2.5.5
修正バージョン: 2.5.5
修正内容:
- 中間層の作成時に、下一段が数字かどうかにかかわらず、常に辞書を使用する。リストの推測は行わない
- 最終値を設定する際、コンテナがリストであり、インデックスが
STORAGE_MAX_LIST_INDEX(10000)以下の場合にのみインデックス処理を行い、超大インデックスは安全にスキップする - 再帰実装を反復実装に変更し、元のコードに潜在的な無限再帰のリスクを排除する
STORAGE_MAX_LIST_INDEX定数をCore/constants.pyに追加し、インデックスの安全上限を集中管理する
修正日: 2026/07/10
再現手順:
# 大数フィールド(例:QQグループ番号)を含むネストされたキーを書き込むと、OOMが発生する
await sdk.storage.aset("groups.871684833.name", "某群")
# → プロセスのメモリが急激に増加し、OOM Killされる
リグレッションテスト: tests/unit/test_unit_storage.py に4つのリグレッションテストを追加
test_nested_key_numeric_segment_as_dict_key— OOMのシナリオを正確に再現test_nested_key_numeric_segment_multiple— 複数の連続する数値フィールドがすべて辞書として扱われるtest_nested_key_existing_list_index_set_within_limit— 既存のリストの有効なインデックスへの書き込みtest_nested_key_list_index_safety_limit— 超大インデックスの安全制限の検証
重大度: 🔴 重大
タイプ: ストレージ
[BUG-025] on_config_update コールバックがコアルーティングに接続されていない
問題: on_config_update(old, new) コールバックは、基底クラス(BaseModule / BaseAdapter)で定義されているが、フレームワークのコアはそのイベントを監視していない。実際の表現では、設定管理パネルで設定を変更するときはトリガーされるが、config.toml を手動で編集するか、setConfig() をコードで呼び出すときは、on_config_update がトリガーされない。
原因: ConfigManager は設定の変更時に config.set / config.updated ライフサイクルイベントを発行するが、これらのイベントを各コンポーネントの on_config_update に転送するサブスクリプションのロジックが存在しない。
根本原因の経路:
コアは config.set / config.updated をサブスクライブしていない
→ 設定変更イベントは転送されない
→ on_config_update は呼び出されない
→ 手動でファイルを編集するか、コードで setConfig を呼び出すと、ホット更新のコールバックがトリガーされない
影響バージョン: 全バージョン
修正バージョン: 2.6.2
修正内容: ModuleManager / AdapterManager は config.set(コード setConfig() パス)と config.updated(手動編集ファイルパス)イベントのサブスクリプションを登録し、設定キーのプレフィックスに一致した後、対応するコンポーネントの on_config_update を呼び出す。型安全な設定オブジェクトを渡す。また、_flush_config() がファイルを書き込んだ後、_config_mtime を同期的に更新していないため、フレームワーク自身の書き込みが外部の変更として誤って config.updated を再発行する問題も修正する。
互換性の説明: 設定のホット更新はフレームワークのコアによって今後は統一的に管理される。以前は設定管理パネルが代行していたロジックは削除され、パネルのアップグレードが必要な場合は、コアとパネルの両方で重複して呼び出される可能性がある。on_config_update のメソッドの署名と意味は変更されていないため、サブクラスは変更する必要がない。
修正日: 2026/07/23
重大度: 🟡 中等
タイプ: 設定システム
[BUG-026] notice/request イベントの reply 目標が誤って推定される
問題: グループ通知イベント(例:メンバーのグループ参加 group_member_increase)で event.reply() を呼び出すと、メッセージはイベントをトリガーしたユーザーのプライベートチャットに送信され、グループではなく通知の対象となる。友達通知イベントでも同様で、返信の対象が混乱する可能性がある。
原因: infer_receive_type() はイベントの detail_type を会話タイプとして直接返している。message イベントでは正しい(detail_type の値 private / group が会話タイプである)が、notice/request イベントの detail_type は意味のサブタイプ(例:group_member_increase、friend_increase)であり、会話タイプではない。その後の convert_to_send_type() と get_id_field() はマッピング表に該当する値が見つからないため、デフォルトの "user" / "user_id" にフォールバックし、返信の対象が間違える。
根本原因の経路:
notice イベント detail_type="group_member_increase"
→ infer_receive_type() は "group_member_increase" を直接返す
→ convert_to_send_type("group_member_increase") はマッピング表にない → "user" にフォールバック
→ get_id_field("group_member_increase") はマッピング表にない → "user_id" にフォールバック
→ target_id = event["user_id"] ← 新メンバーのプライベートチャット(グループではなく)
影響バージョン: 全バージョン
修正バージョン: 2.7.0-dev.3
修正内容: infer_receive_type() は、detail_type が既知の会話タイプ(標準タイプまたはカスタムタイプ)である場合にのみ、その値を直接返すようにする。それ以外の場合は、ID フィールド(group_id / channel_id / user_id など)に基づいて正しい会話タイプを推定する。
リグレッションテスト: tests/unit/test_unit_session_type.py → TestNoticeRequestTypeInference(10 用例)
修正日: 2026/07/29
重大度: 🟢 軽度
タイプ: イベントシステム
[BUG-027] ルーティングのリクエスト制限クリーンアップタスクが固定ウィンドウを使用して長時間のルールが無効になる
問題: ルーティングのリクエスト制限を長時間のルール(例:100/hour、{"requests": 100, "window": 3600})に設定した場合、制限は無効化され、実際の表現は 100/minute(1時間で約6000回のリクエストが可能)に近い。1時間単位の保護効果は全く期待できない。
原因: _apply_rate_limit は実際の window(最高3600秒)を取得するが、per-request のチェックは確かに3600秒の保留時間を使う。しかし、バックグラウンドのクリーンアップタスク _cleanup_expired_rate_limits は、固定定数 DEFAULT_RATE_LIMIT_WINDOW_SECS(60秒)をすべてのルーティングの共通のクリーンアップ閾値として使用する。したがって、60秒前のタイムスタンプはすべてクリアされ、1時間のウィンドウ内では常に最近1分間の記録しか残らない。100/hourは実際には100/minute(60倍緩和)に退化する。
根本原因の経路:
_apply_rate_limit は window=3600(100/hour)を解析する
→ per-request 検査は 3600s 保留のタイムスタンプを使う(正しい)
→ しかし _cleanup_expired_rate_limits は固定 max_window=60s を使用してクリーンアップする
→ 60s 前のタイムスタンプはすべてクリアされる
→ 1時間のウィンドウは常に最近1分間の記録しか残らない
→ 100/hour は実際には ~100/minute に退化する(60倍緩和)
影響バージョン: 2.6.0-dev.0 - 2.7.0-dev.4
修正バージョン: 2.7.0-dev.5
修正内容: 新たに _rate_limit_windows: dict[str, int] を追加して各ルーティングの実際のウィンドウを記録する。_apply_rate_limit が最初にエントリを作成する際にウィンドウを書き込む。_cleanup_expired_rate_limits は各 key ごとの実際のウィンドウでクリーンアップする(存在しない場合はデフォルト値に回帰)。クリーンアップと stop() 時に両方の辞書を同期して維持する。
修正日: 2026/07/31
リグレッションテスト: tests/unit/test_unit_router.py → TestRateLimit::test_cleanup_respects_per_route_window
重大度: 🔴 重大
タイプ: ルーティング
[BUG-029] 設定監視タスクが未完成の TOML をブロードキャストし、例外を静かに飲み込む
問題: ユーザーが config.toml を半分保存している間に(一時的な構文エラーが発生する)、設定監視のバックグラウンドスレッドは mtime の変化を検出し、設定を再読み込みするが、読み込みに失敗しても空の設定 {} を使って config.updated イベントをブロードキャストするため、アダプタ/モジュールの on_config_update は空の設定を受け取り、すべての設定項目がリセットされたと誤認してデフォルト値に戻る。さらに、監視ループは except Exception: pass を使用してすべての例外を静かに飲み込み、ウォッチャーの障害を確認できない。
原因: 2つの欠陥が重なっている:
_load_configは TOML 構文エラー/権限エラーの際にself._cacheを{}に上書きするが、バックグラウンド監視スレッド_watch_loopとキャッシュの有効期限パス_check_cache_validityは、_load_config()が成功した後に必ず_emit_config_updated()を実行するため、"読み込み失敗で生じた空のキャッシュ"を真の変更としてブロードキャストする。_watch_loopのexcept Exceptionは警告レベルでログを記録するように変更された(新しい i18n キーcore.config.watcher_errorを追加し、5言語で同期)。
根本原因の経路:
ユーザーが保存中に TOML 構文エラーが発生する
→ _load_config() は _cache = {} に上書きする
→ _watch_loop は _emit_config_updated(new_config={}) を無条件に実行する
→ アダプタ/モジュール on_config_update は空の設定を受け取る
→ 設定がリセットされたと誤認してデフォルト値に戻る
影響バージョン: 2.6.2-dev.1 - 2.7.0-dev.4
修正バージョン: 2.7.0-dev.5
修正内容:
_load_configはboolを返すように変更する。TOML 構文エラー/権限エラー/その他のエラーの際には、self._cacheを上書きせず、診断ログを記録してFalseを返す。_watch_loopと_check_cache_validityは、_load_config()がTrueを返した場合にのみconfig.updatedをブロードキャストする。_watch_loopのexcept Exceptionを警告レベルでログを記録するように変更する(新しい i18n キーcore.config.watcher_errorを追加し、5言語で同期)。
修正日: 2026/07/31
リグレッションテスト: tests/unit/test_unit_config.py → test_malformed_toml_preserves_last_valid_cache、test_permission_denied_logs_clear_message(更新して有効なキャッシュを保持し、False を返すことを検証する)
重大度: 🟡 中等
タイプ: 設定システム
[BUG-030] 設定 watcher の競合により setConfig 延期書き込みが静かにデータを失う
問題: config.setConfig(key, value)(デフォルト immediate=False)後に、他のモジュールの設定が書き込まれるが、自分のモジュールの設定は書き込まれず、他のモジュールの設定は正常に動作する。immediate=True(強制的な書き込み)に設定することで回避できる。実行時に書き込まれた設定は、再起動後に失われる。起動時に生成されたテンプレートの設定は保持される。
原因: 2つの重なる欠陥:
- 論理的な欠陥:
_watch_loopが_check_file_change()がTrueを返した場合、_dirty_keys.clear()で全ての待機キーを破棄する。しかし、_check_file_change()は!=で mtime を比較するだけなので、フレームワーク自身の_flush_configが書き込み後に_config_mtimeを更新しても、watcher スレッドがファイルの書き込みと mtime 代入の間(および粗粒度のファイルシステム上)で mtime の差異を観測し、外部の変更と誤認して全ての待機キーを破棄する。 - スレッドの欠陥:
_watch_loopが_write_timer/_dirty_keysを操作する際に_lockを保持していない。setConfig(_dirty_keysに書き込む際にロックを持つ)、_schedule_write(_write_timerに書き込む際にロックを持つ)とデータ競合がある。
根本原因の経路:
モジュールA setConfig(immediate=True) → flush でファイルに書き込み、mtime が変わる
→ ユーザーモジュール setConfig(immediate=False) → _dirty_keys に入り、5秒後に書き込み
→ watcher ループが観測し、_check_file_change が自身の書き込みの mtime 差異を観測する
→ _dirty_keys.clear() → ユーザーモジュールの待機キーが静かに破棄される
→ 再起動後に設定が失われる
影響バージョン: 2.6.0 - 2.7.0
修正バージョン: 2.7.1
修正内容:
_last_self_write_mtimeフィールドを追加し、_flush_configがファイルに書き込む際に更新する。_check_file_changeは mtime の変化を観測する際に、この値と一致するかを比較し、自身の書き込みと判断してFalseを返す。_watch_loopの全体を_lockで保持する。外部の変更が発生した場合、_dirty_keysを保持する(マージの意味)。次の flush は外部内容とマージする(汚染されたキーが優先される)、_dirty_keysを破棄しない。getConfig/_check_cache_validityパスは影響を受けない(reloadは待機キーを破棄しない)。
修正日: 2026/08/06
リグレッションテスト: tests/unit/test_unit_config.py → test_self_write_not_detected_as_external、test_external_change_preserves_dirty_keys、test_flush_merges_dirty_with_external
重大度: 🔴 重大
タイプ: 設定システム
[BUG-032] 設定の遅延書き込み中に「書き込み直後に読み取り」で古い値が返される
問題: config.setConfig()(デフォルト immediate=False で約5秒遅延)で点分キーを書き込んだ後、その親/祖先ノード(例: set_erispulse_section("scope.actions.MyModule", {...}) の後で get_erispulse_config() を呼び出す)を読み取ると、古い値が返り、書き込んだ子キーは「消え」、書き込み後にのみ見える。作用域設定のホットアップデートなどの「書き込み-読み取り-書き込み」シナリオに影響される(2.8.0 テストプラグイン /t_section 用例で暴露)。
原因: setConfig は点分キーをフラットな形式で待機キュー _dirty_keys に保存し、getConfig の正確なキー照合は待機キューに命中する。ツリー形式のパスの照合(getConfig("ErisPulse.scope"))はキャッシュツリーにのみ依存し、待機値を追加しない——遅延書き込み(_flush_config が汚染された値をキャッシュにマージし、キューをクリアする)の間、読み-あなた-書き込みの断層が発生する。
影響バージョン: 2.6.0 - 2.8.0-dev.1
修正バージョン: 2.8.0-dev.1
修正内容: getConfig に待機値のマージロジックを導入する——① 精確なキーが待機キューに命中した場合、元の値を返す(既存の動作は変更しない);② 待機キーが照合キーの祖先の場合、最も長い待機祖先を取得し、残りのパスをその値のサブツリーで解析する;③ 待機キーが照合キーの後代の場合、_dirty_overlay を使って深くマージする(_deep_merge、上書き優先、元のキャッシュオブジェクトを変更しない)。待機キーがない場合は元の高速パスを走る、追加のオーバーヘッドゼロ。
修正日: 2026/09/04
リグレッションテスト: tests/unit/test_unit_config.py → test_get_config_overlays_dirty_descendant、test_get_config_overlay_merges_with_cache_siblings、test_get_config_overlay_new_branch、test_get_config_dirty_ancestor_query、test_get_config_dirty_exact_key_still_wins
重大度: 🟡 中等
タイプ: 設定システム
[BUG-033] wait_reply が待機中の返信が高優先度のハンドラに食い込まれる
問題: モジュールが wait_reply() でユーザーの返信を待っている間に、その返信メッセージが高優先度のイベントハンドラに mark_processed() で認定されると、コマンドディスパッチャは _processed フラグを確認して即座に返り、_check_pending_reply が _handle_message の末尾に位置するため、返信のマッチングが実行されない——待機側は返信を受け取らず、タイムアウトまで None を返す。典型的なトリガー状況:高優先度のメッセージハンドラ(記録/監査/ブロック系)を使用するロボットでは、対話的なインタラクションがランダムに失敗する。
原因: 返信のマッチング _check_pending_reply が _handle_message の末尾に位置する(_processed チェックの前に)、_processed チェックが返信のマッチング判定よりも先に実行される。会話の継続はフレームワークレベルのセッション継続メカニズムであり、他のハンドラの認定に影響されるべきではない。
影響バージョン: 2.2.0-dev.0 - 2.8.0-dev.1
修正バージョン: 2.8.0-dev.2
修正内容: 返信のマッチング判定を _handle_message の入口に移動する(_processed チェックの前に、メッセージイベントにのみ)。まず会話の完了を試み、マッチングされた場合、イベントは mark_processed でマークされ、下のチェックは短絡的に実行される。マッチングされない場合は、元のコマンドマッチングの流れに従う。また、判定チェーンを新しいセッション管理器(Core/Event/interaction.py)に委譲し、所有者によるキャンセルと権限の再確認機能を順次獲得する。
修正日: 2026/09/08
リグレッションテスト: tests/unit/test_unit_interaction.py(TestRegisterResolve マッチ/未マッチ/認定フラグ)、tests/unit/test_unit_event.py(wait_reply 全チェーン)
重大度: 🟡 中等
タイプ: イベントシステム / コマンドシステム
[BUG-034] persist=False のランタイムバインディングが任意の後続の設定書き込みによって破壊される
問題: scope.set_module(..., persist=False) などのランタイムバインディングはメモリ self._data にのみ書き込まれる。しかし、scope は config.set / config.updated イベントをサブスクライブしており、任意のコードが設定を書き込む(例:他のモジュールが独自のデフォルト設定を書く)と、scope は設定ファイルから全体の設定ツリーを再構築し、以前のすべてのランタイムバインディングが静かに失われる(判定はデフォルトの許可に戻る)、ログの提示もない。ランタイムバインディングに依存するシナリオ(Dashboardの「ランタイムのみ」スイッチ、モジュールのランタイム時の動的無効化)は、無関係なモジュールが設定を書き込んだ後に動作が戻る。
原因: 根本原因の経路: scope.set/delete(persist=False) はメモリにのみ書き込む(Core/scope.py)→ 任意の setConfig が config.set イベントをトリガー → _on_config_updated は _load_config() を無条件に呼び出す → _apply_tree() は self._data = {...} で全体を置き換える → 設定ファイルにないランタイムバインディングは破棄される。
影響バージョン: 2.8.0-dev.1 - 2.8.0-dev.2
修正バージョン: 2.8.0-dev.2
修正内容: ランタイムオーバーライド層 _runtime_overrides(削除のセントリーン)を導入する: persist=False で書き込み/削除はオーバーライド層に記録され、_apply_tree() が永続層を再構築した後に、オーバーライド層を再適用する。persist=True で書き込み/削除はオーバーライド層から対応する記録をクリアする(ユーザーの永続化の意思を優先)。config.set はイベントの key で正確にフィルタリングされ、config.updated は新旧の scope 節の比較を行い、scope が実際に変化した場合にのみ再構築する(不要な書き込みが判定 LRU キャッシュを破壊するのを回避)。unregister_by_owner() を追加して、モジュールがアンロードされたときに所有者とともにクリーンアップする。Core.Event.overrides の persist=False のランタイムオーバーライドも同様にオーバーライド層アーキテクチャで修正される。
修正日: 2026/09/09
再現手順: ① scope.set_module("testplat", blocked=["TestB"], persist=False) → 判定 False;② 任意のモジュールが config.setConfig("HelpModule", {...}) を実行 → scope 再構築がトリガーされる;③ scope.is_allowed("testplat", None, "TestB") は True を返す(期待値は False である)。
関連: Issue #432
リグレッションテスト: tests/unit/test_unit_scope.py::TestRuntimeOverrideSurvival(不要な書き込みが生き残る / 樹形再構築で再適用 / 削除のセントリーン / 永続化でクリア / 精確な無効化 / owner 清理)
重大度: 🟡 中等 タイプ: 設定システム / 実行時
[BUG-035] 設定パネルの select 選択肢と dict フィールドが [object Object] にレンダリングされる
問題: WebUI 設定パネルで、select フィールドのオプションのドロップダウンは [object Object] と表示される(動的に生成された配色スタイルのオプションなど);stalker_mode、knowledge_base などのネストされた設定セグメントの dict フィールドは、テキストボックスに [object Object] と表示され、正しく表示・編集できない。
原因: 2つの独立した欠陥:① フレームワークの i18n 解析器 _resolve_i18n_text は i18n キーを持つ字典を復元するだけであり、オプションのラベルが default だけを含む字典(i18n キーのない動的テキスト)の場合はそのまま渡され、esc(label) で文字列に強制変換され [object Object] となる;② Dashboard レンダリングの分岐は array 型に対して JSON textarea を行い、dict 値はテキスト入力の分岐に落ちて String() で強制変換される。さらに、モジュールが _schema_meta を普通の dataclass フィールド(ClassVar 注釈がない)として誤って宣言すると、設定フィールドとして schema に混入し、混乱を悪化させる。
影響バージョン: 2.7.0 - 2.8.0-dev.2
修正バージョン: 2.8.0-dev.2
修正内容: ① _resolve_i18n_text は default だけを含む字典をテキストに復元するようにする;② フレームワークの schema/テンプレート/デフォルト値/検証/埋め込みの5カ所でアンダースコアで始まるフィールドを除外する(誤った宣言は無害化);③ Dashboard select オプションのラベルはオブジェクトをデフォルトで解析し、dict/table フィールドは JSON textarea にレンダリングする(保存パスは tp=object で JSON.parse に戻し、完全な往復)。
修正日: 2026/09/09
リグレッションテスト: tests/unit/test_unit_config.py::TestResolveI18nDefaultOnlyDict、TestSchemaUnderscoreFieldExclusion
重大度: 🟡 中等 タイプ: 設定システム
[BUG-036] 複数インスタンスで共有の設定ディレクトリを使用する場合、設定の書き込みが偶発的に失敗する(ENOENT)
問題: Docker 部署環境(複数のコンテナが同一のホスト設定ディレクトリをマウント)では、ログに連続して2行の Failed to write configuration file ... [Errno 2] No such file or directory: '...config.toml.tmp' -> '...config.toml' が表示される。この設定書き込みは破棄される(古い設定は完全に保持され、設定の欠落は観察されない)、機能に影響はないが、トラブルシューティングを妨げるアラートが繰り返し表示され、待機中の設定項目は次の書き込みまで保存されない。
原因: 根本原因の経路: _flush_config / setConfigTemplate は固定名の一時ファイル config.toml.tmp を使用して新しい内容を保持し、write() の後に fsync を実行せずに rename() を直接行う。2つの ErisPulse インスタンスが同一の設定ディレクトリを共有している場合、B インスタンスの open("w") は A インスタンスが書いている一時ファイルを truncate する可能性がある → A の rename は、ターゲットが B によって取られたり、内容が切断されている可能性があるため、ENOENT(ユーザーのログに表示される2回のエラー)が発生する。報告されたケースでは、書き込み失敗のアラートが表示されるだけで、古い設定は保持されている。極端なタイミングが交差した場合、rename は空または半分の config.toml を出力する可能性がある(現実の環境ではまだ爆発していない)。単一インスタンス環境では、ext4 の遅延割り当てにより「rename のメタデータがデータブロックのドアを閉じる前に実行される」クラッシュの窓が存在する(SIGKILL / 電源切断)。_file_lock はプロセス内 threading.RLock であり、プロセス間/コンテナ間の書き込みには制約がない。
影響バージョン: 2.2.0-dev.0 - 2.8.0
修正バージョン: 2.8.1
修正内容: 設定の書き込みはすべて _atomic_write_text() に収束する:同じディレクトリで mkstemp を使ってプロセス固有の一時ファイルを生成する(固定名の競合を排除し、複数インスタンスでは最後の書き込み勝者になる、ENOENT は発生しない)→ 書き込み後に flush + fsync を強制してデータをドアに落とす(「rename が有効だが、データがドアに落とされていない」窓を削除)→ os.replace で原子的にターゲットを置き換える(POSIX/Windows ともに原子的、ある時点でディスク上には完全な古い内容か、完全な新しい内容のいずれかがある);POSIX では設定ディレクトリの fsync を追加する。_flush_config、setConfigTemplate、ルートディレクトリの設定移行の3つの書き込みポイントをすべて切り替える。例外パスのクリーンアップロジックは一時ファイル名の再構築に伴って再構築される。複数インスタンス検出を追加する:起動時にアドバイザリロック(POSIX flock / Windows msvcrt.locking)で設定ディレクトリのロックファイル .erispulse_config.lock を排他的に保持し、占有されている場合は i18n アラートを出力する(起動をブロックしない)、ロックは OS がプロセス終了時に自動的に解放し、幽霊ロックがない。
修正日: 2026/09/13
再現手順: ① 2つのコンテナが同一のホスト config/ ディレクトリをマウントして同時に ErisPulse を実行する;② いずれかのインスタンスが設定の書き込みをトリガーする(例:モジュールがデフォルト設定を登録);③ ログに ENOENT の書き込み失敗アラートが表示され、今回の書き込みは破棄される(古い設定は保持される)。
関連: ユーザー報告(1Panel コンテナ ×2)
リグレッションテスト: tests/unit/test_unit_config_atomic_write.py(内容が完全に書き込まれる / 一時ファイルが残らない / 書き込み失敗で元のファイルが保持される / 2つのインスタンスが並行して書き込むファイルは常に合法 / ロックファイルの作成 / 複数インスタンスのアラート / 移行時の原子的な書き込み)
重大度: 🟢 軽微
タイプ: 設定システム
[BUG-037] 空白区切りのコマンド名で登録したサブコマンドは決してトリガーされない
問題: 空白で区切られた複数トークンのコマンド名でサブコマンドを登録する(例:@command("admin add"))と、コマンドは正常に登録され、ヘルプリストにも表示されるが、ユーザーが /admin add を送信すると、ロボットは応答せず、入力は /admin として add がパラメータとしてマッチする。admin が登録されていない場合、完全に応答しない。ピリオド区切りの名前(admin.reload、単一トークンとして全体)を使用する場合にのみ回避できる。
原因: 根本原因の経路: CommandHandler.__call__ は任意のコマンド名(空白形式)をそのまま平らな self.commands 辞書に保存する → 分発段階の _try_execute_command はメッセージの最初のトークンのみでマッチする(cmd_name = parts[0])→ 複数トークンのコマンド名は辞書のキーとして決して検索されない。登録と分発の2段階でコマンド名の空間の仮定が一致しておらず、登録時に警告が発生しない(静黙で無効)。
影響バージョン: コマンドシステム導入から - 2.8.0
修正バージョン: 2.8.1
修正内容: 分発層を最長プレフィックスマッチに変更する:" ".join(parts[:n])(n の上限は登録されたコマンド名/エイリアスの最大トークン数のキャッシュ _max_name_tokens)から降順で試行し、一致すれば残りのトークンをパラメータとして実行し、下流の作用域/ACL/オーバーライド/マスター/権限チェーンはコマンド全体名で自然に有効になる。対応する意味:親子が共存する場合、未登録のサブコマンド入力は親コマンドに降格する(歴史的な動作は変更なし);サブコマンドが permission を宣言していない場合、直近の祖先コマンドの権限を継承する(親コマンドを保護する=その下のすべてのサブコマンドを保護する);単一トークンのコマンドのみが登録されている場合、最初の試行で一致し、分発のオーバーヘッドは以前と同等。unregister / unregister_by_owner / 全量クリーンアップもトークン数のキャッシュを維持する。
修正日: 2026/09/13
再現手順: ① モジュール内で @command("admin add") を登録;② /admin add x を送信;③ 修正前は応答せず(または登録された /admin にパラメータとしてマッチする)、修正後は admin add がトリガーされ、get_command_args() は ["x"] となる。
リグレッションテスト: tests/unit/test_unit_command_subcommand.py(最長プレフィックスマッチ / 単一サブコマンドがトリガーされる / 3段階ネスト / 大文字小文字の2モード / 単一と複数トークンのエイリアス / イベントペイロードの全名 / ライフサイクルフックの全名 / 権限継承6例 / ACL glob 全名 / master / アンロードの降格とキャッシュの再計算)
重大度: 🟡 中等
タイプ: イベントシステム
[BUG-038] persist=False のランタイムバインディングが永続化書き込みに伴い磁盤に復活する
問題: scope.set(path, value, persist=False) で書き込まれたランタイムバインディング(ドキュメントに記載されているように、磁盤に落とさず、プロセス再起動後に失効し、モジュールのアンロード時にクリーンアップされる)は、任意の persist=True の書き込み(デフォルト値、例:モジュール set_module)によって差分書き込みされ、ユーザーの config.toml に落とされる。その後、モジュールがアンロードされてランタイムバインディングがクリーンアップされても、再起動後の設定再読み込み時にそのバインディングは磁盤から「復活」して有効であり、プロセス再起動後も依然として存在する——「ランタイムバインディングは磁盤に落とさない」の語義契約に反し、非常に難解。
原因: 根本原因の経路: ScopeManager.set() は値をメモリの設定ツリー _data に直接書き込む(ランタイムバインディングも同様に _data に直接書き込む)→ 永久化の分岐は全体の _data を深コピーしたスナップショットを update_erispulse_config に差分書き込みに渡す → スナップショットには persist=False のバインディングの値が混入する。delete(persist=True) 同様:parent ノードの有効な参照を遅延書き込みキューに渡し、遅延書き込み中にそのノードの兄弟キーのランタイム変更が一緒に書き込まれ、有効な参照が存在する間に汚染された窓が発生する。
影響バージョン: 2.8.0-dev.2 - 2.9.0-dev.0
修正バージョン: 2.9.0-dev.1
修正内容: 永久化のベースライン _persisted_tree(設定ファイルから読み込み、検証済みのツリーで更新される)を導入する:set(persist=True) はベースラインに適用された変更を差分書き込みに渡すだけで、ランタイムバインディングは永続化内容に含まれない。"書き込み直後に読み取り"はメモリの最終状態スナップショットから直接復元し、ベースラインの再構築で汚染されたベースラインを避ける。delete(persist=True) 同口径:ベースラインに適用された削除を差分書き込みに渡し、ベースラインのサブツリーの深コピーを永続化層に渡す。set_action は全体の置き換えとして、古い規則キーが永続化内容に残らないように、削除して書き込む。
修正日: 2026/09/27
再現手順: ① モジュール内で scope.set("bots.p.debug_mode", {"blocked": ["X"]}, persist=False) を実行;② 任意の永続化書き込み(例:他のモジュールが set_module を呼び出す)をトリガーする;③ config/config.toml を開き、debug_mode が書き込まれている(修正前);④ モジュールをアンロード(ランタイムバインディングがクリーンアップされる)した後、設定を再読み込みすると、scope.get("bots.p.debug_mode") はまだバインディング値を返す。
リグレッションテスト: tests/unit/test_unit_scope.py::TestPersistBaseline(ランタイムバインディングは無関係な永続化書き込みに伴い磁盤に落とされない / アンロード後の設定再読み込みで復活しない / delete はベースラインのサブツリーを渡し、ランタイムの兄弟キーを含まない / set_action は置き換えで残りのキーを残さない / cache_size 設定が有効)
重大度: 🔴 重大
タイプ: 設定システム
[BUG-039] 節全体の書き込みと点分書き込みが混在する場合、読み書きが不一致になる(点分の上書きが失われる)
問題: 遅延書き込みのウィンドウ内で、同じ節に全体の書き込み(setConfig("Mod", {...})、例:BaseModule.cfg が戻る)と点分の書き込み(setConfig("Mod.key", v)、例:設定のホット更新 / テストツールによる上書き)が混在する場合、2つのバリエーションがある:バリエーション A(読み取りの経路)——getConfig("Mod") で節全体を読み取ると、古い節の待機スナップショットが返され、後で行われた点分の上書き(点分の読み取り経路は正常)は見えない;バリエーション B(書き込みの経路、より深刻)——点分の書き込みが先で、全体の書き込みが後(テストツールが注入した上書き → モジュールが self.cfg = ... で戻す)の場合、flush は挿入順で待機キーを適用し、全体の書き込みが点分の上書きの後に来ると、点分の上書きが磁盤上に永久に失われる。生産環境では「設定のホット更新 + モジュールの実行時書き込み」の組み合わせが発生する可能性があり、テスト環境とは無関係(ErisPulse-DailyCard テストフィードバックが暴露)。
原因: getConfig 第①段(正確に待機キーにマッチ)は return self._dirty_keys[key] を返し、第④段 _dirty_overlay の後代の待機値を完全に迂回する;_flush_config は待機キーを挿入順で適用するので、全体の書き込みが点分の上書きの後に来ると、点分の上書きが磁盤上に永久に失われる。さらに、第①段で待機値を返すと、getConfig は元のオブジェクト参照を返し、呼び出し側が返された辞書を直接変更すると、待機状態が変更される。
影響バージョン: 2.6.0 - 2.9.0-dev.1
修正バージョン: 2.9.0-dev.1
修正内容: 待機ウィンドウ内では特異性優先のロジックを採用する——点分(より具体的)の待機値は全体(より広い)の待機値を上書きする。読み取りと書き込みの両方で同じロジックを適用する:① getConfig で正確に待機キーにマッチした後、_dirty_overlay の後代の待機値を追加する(非 dict 値は返す、③+④の標量エッジと同口径);② _flush_config は待機キーをパスの深さ順で適用する(全体/祖先は先、点分/後代は後、安定ソートで同じ深さの待機キーの順序を維持)。代償:同じ待機ウィンドウ内(約5秒)では、全体の書き戻しはまだ保留中の点分の上書きを上書きできない——読み取りの修正後は、読み-変更-書き込みの自然な連携で点分の値が含まれる。実際の影響範囲は非常に小さい。③ 待機値を返す getConfig は(正確にマッチ / 祖先のサブツリー / 重ね合わせのマージ)を copy.deepcopy で隔離コピーする。待機キーがない場合の高速パスと純粋なキャッシュ読み取りの動作は変更なし。
修正日: 2026/09/28
再現手順: ① setConfig("FB.y", 1);② setConfig("FB", {"z": 2});③ force_save()——修正前は磁盤に [FB] z = 2 しか残らず(y は失われる)、修正後は {"y": 1, "z": 2};バリエーション A:ステップ①②の後、待機しないで getConfig("FB") を実行する——修正前は y が含まれず、修正後は見える。
リグレッションテスト: tests/unit/test_unit_config.py::TestSectionAndDottedDirtyConsistency(バリエーション A 2つの挿入順 / バリエーション B は磁盤と順序に関係なく / 3段階混合で生き残る / 隔離コピー / 祖先のサブツリーの隔離)
重大度: 🟡 中等
タイプ: 設定システム