简体中文 English 繁體中文 日本語 Русский
本文为静态镜像,内容以交互版为准 在交互式文档中心打开 →

name: erispulse-shadow-transcript description: "适用于:新旧版本灰度对照(shadow)、消息收件箱与行为追踪(transcript)。覆盖场景:このドキュメントを読むべきタイミング:カナリアリリース——新バージョンの並行トライアル実行、動作比較、昇格・ロールバックが必要なとき。不要であればスキップしても、他の機能の利用には影響しません。このドキュメントを読むべきケース:メッセージ履歴や会話の復元が必要なとき、または受信トレイの保持ポリシーを調整したいとき。不要であれば読み飛ばしても、他の機能の利用には影響しません。"

ErisPulse 影子灰度与消息追踪

This skill is auto-built from the ErisPulse documentation. Repository: https://github.com/ErisPulse/ErisPulse Source paths are annotated per section; see docs/ in the repository for the latest content.

内容大纲

AI 按需读取:只读需要的章节即可,不必全文加载。

影子模块

来源: docs/ja/advanced/shadow.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ja/advanced/shadow.md)

シャドウモジュールとカナリア昇格

Tip

このドキュメントを読むべきタイミング:カナリアリリース——新バージョンの並行トライアル実行、動作比較、昇格・ロールバックが必要なとき。不要であればスキップしても、他の機能の利用には影響しません。

読み終えるとわかること:シャドウモジュールの5つの分離ゲート、動作 diff のアライメント方法、 promote による昇格と失敗時のロールバック手順、そしてフレームワークでは防げない誠実な限界。

このドキュメントでは扱わないこと:通常モジュールのホットリロードとオーナーのクリーンアップに関する汎用メカニズム (オーナー(owner)システム を参照)。

シャドウ = 同一モジュールの新バージョンであり、独立した owner(例:roll_shadow)として本番の旧バージョンと 共存しながらトライアル実行します:実際のイベントのコピーを受け取り、そのアウトバウンドは本当に送信されるのではなく 傍受して記録されます——shadow_diff で2つのバージョンの動作を比較し、無害であることを確認したら promote で ワンクリック昇格、dismiss でいつでも破棄できます。モジュールコードは一切変更不要、全工程がランタイム API 駆動: load / unload / reload と同種の運用アクションであり、Dashboard / カスタム管理モジュールから直接 呼び出せ、書くべき設定項目は一切ありません。

{!--< tips >!--}

  1. 起動:await sdk.module.shadow_start("roll", source="path/to/v2") ——新バージョンのコードは独立した owner(デフォルトはパス名)として旧バージョンと共存
  2. シャドウは実際のディスパッチと依存グラフには参加しません:同名コマンドはシャドウのカタログに入り、ルートは登録のみでマウントせず、 ライフサイクルブロードキャストはサイレント、module.call と依存解決は引き続き v1 を指します
  3. 昇格は常に人間が確認します:await sdk.module.promote_shadow("roll")、失敗時は 自動ロールバックで旧インスタンスがサービスを継続;dismiss_shadow でいつでも破棄 {!--< /tips >!--}

クイックスタート

# v2 コード:通常のモジュールの書き方、シャドウを一切意識しない(任意のディレクトリ、例:downloads/roll_v2/)
# 本番ボット内で(Dashboard / 管理モジュールから呼び出し)、1行でカナリアを起動:
await sdk.module.shadow_start("roll", source="downloads/roll_v2")
# → シャドウは独立した owner "roll_v2" として v1 と共存し、アウトバウンドは傍受・記録される

# トライアル実行中に動作を比較:
report = sdk.module.shadow_diff("roll")
# {"shadow_owner": "roll_v2", "count": 3, "aligned": [...]}

問題ないことを確認したら昇格:

await sdk.module.promote_shadow("roll")   # 昇格、失敗時は自動的に v1 へロールバック
await sdk.module.dismiss_shadow("roll")   # または:シャドウを破棄

5つの分離ゲート

ゲート メカニズム
イベントコピー シャドウハンドラーはイベントの独立したコピーを受け取る(shadow マーク付き)——シャドウによる改変 / 引き取り / 伝播停止はコピーにのみ作用し、元のイベントチェーンには影響しない
アウトバウンドゲート シャドウの Send DSL と Api 呼び出しはすべて傍受して記録(成功形状のフェイクレスポンスを返却)、実際には送信しない——シャドウが重複して返信することはない
ストレージオーバーレイ シャドウの KV 書き込みはメモリ上のオーバーレイに入り、実DBへの永続化は破棄;読み取りはまずオーバーレイを確認し、ミスなら実DBへ透過(カナリアは実際のデータに対して実行);削除はトゥームストーンとして記録
ルートブロック シャドウの HTTP/WS/SSE ルートは登録のみでマウントしない;同名コマンドはシャドウのコマンドカタログに入り、プラットフォームイベントメソッドの注入は禁止
ライフサイクルサイレント シャドウは自身のライフサイクルイベントをブロードキャストせず、エコシステムの依存グラフに参加しない(module.call と依存解決は引き続き v1 を指し、未完成品が依存されるのを回避)

設定の継承:シャドウはデフォルトで元モジュールの設定セクションを継承します(そうでないとカナリアの結果が歪みます)。昇格後は設定がそのまま有効になります。

誠実な限界(防げないもの)

昇格とロールバック

promote の流れ:現在のバージョンをスナップショット(カスケード依存モジュールを含む)→ 完全アンロード → シャドウを本名で登録・ ロード → いずれかのステップが失敗すれば自動ロールバック、旧インスタンスがサービスを継続(ベストエフォートのセマンティクス:on_unload で 既に実行された副作用は取り消し不可、ロールバック後の旧インスタンスは終了処理済みの状態)。昇格成功後、シャドウのリソースは 回収され、バインドは解除;元モジュールの設定セクションがそのまま有効になります。

永続化の注意:promote はランタイムでの切り替えです——再起動後も v2 で動作し続けるには、新バージョンを 永続的にインストールする必要があります(pip install -U で新バージョンを導入 / プラグインファイルを差し替え)。 ランタイムでの切り替えはパッケージ管理を代行しません。

関連ドキュメント

消息追踪

来源: docs/ja/advanced/transcript.md(https://github.com/ErisPulse/ErisPulse/blob/main/docs/ja/advanced/transcript.md)

セッション受信トレイ(Transcript)

Tip

このドキュメントを読むべきケース:メッセージ履歴や会話の復元が必要なとき、または受信トレイの保持ポリシーを調整したいとき。不要であれば読み飛ばしても、他の機能の利用には影響しません。

セッション受信トレイは、ボットが送受信したメッセージを自動的に記録し(セッションキー platform:detail_type:target_id 単位で整理)、 会話の復元(event.history)、シャドウモジュールの diff、コールドスタートのリプレイなどに履歴データを提供します。 このドキュメントでは、そのストレージモデルと、保持ポリシーの各調整ノブがそれぞれ何を保護しているのか、実行時オーバーライド API、そして制限を緩和することの実際のコストについて説明します。

制限を変更する前に、必ず「リスクと監査」のセクションを最後までお読みください——各調整ノブには、それが存在する理由があります。

動作原理

メッセージ ──append()──▶ 書き込みバッファ(メモリ上の deque、ハード上限 4096 行)
                        │  1 秒ごとにバッチでディスクへ書き込み(1 バッチ ≤64 行、単一トランザクション)
                        ▼
              storage バックエンドの transcript テーブル(デフォルト sqlite)
                        │  約 32 件の追記ごとに遅延トリガーで保持クリーンアップを 1 回実行
                        ▼
              ┌─ セッション別 FIFO トリミング(max_per_session、デフォルト 50 件)
              └─ グローバル TTL 削除(ttl_hours、デフォルト 168 = 7 日)

3 つの階層の保護があり、それぞれが異なるリスクを防いでいます:

ノブ デフォルト値 保護対象 無効化(=0)した場合の影響
書き込みバッファ上限 4096 行(定数、設定不可) メモリ:ストレージ利用不可期間のバックログが RAM を圧迫しない 無効化不可——これはメモリの防衛線です
単一テキストの切り詰め 2000 文字(定数、設定不可) 単一メッセージのメモリ/ディスク消費 無効化不可
max_per_session 50 件/セッション ディスク増加:単一セッションの履歴が無限に蓄積すること そのセッションの履歴が無限に増加
ttl_hours 168(7 日) ディスク増加:全セッションの長期的な蓄積 transcript テーブルが無限に増加

重要なポイント:書き込みバッファにはハード上限があるため、保持ポリシーを緩めてもメモリが直接圧迫されることはありません。 真のリスクはストレージ層と下流のコンシューマーにあります(次のセクションを参照)。

Tip

セッションメモリを必要としないアプリケーションは全体を無効化できます:ErisPulse.transcript.enabled = false とすることで、受信/送信メッセージの自動記録が停止します——メモリバッファにもストレージにも書き込まれず、常駐メモリと継続的な ストレージ層 IO を節約できます。セッションインボックスは主に AI コンテキスト注入とリピート防止という 2 つのニーズに対応しており、 どちらも不要なデプロイ(純粋なコマンド型ボットなど)では無効化を推奨します。

設定

[ErisPulse.transcript]
enabled = true          # 自動記録するかどうか
max_per_session = 50    # セッションごとの保持件数(0 = このセッションのトリミングを無効化)
ttl_hours = 168.0       # グローバル保持期間(時間、0 = クリーンアップしない)
self_echo = "bot"       # 着信 bot エコー処理:bot / skip / user

環境変数による上書きも同様に有効です:ERISPULSE_TRANSCRIPT_MAX_PER_SESSION、ERISPULSE_TRANSCRIPT_TTL_HOURS、 ERISPULSE_TRANSCRIPT_SELF_ECHO。 設定変更はホットリロードで反映されます(書き込みバッファのディスクフラッシュ間隔内で、最大約1秒の遅延)。

Tip

セッションメモリが不要ならオフにしましょう:インボックスの唯一の消費者は会話復元 / シャドウ diff / コールドスタート再生です。 ボット(またはデプロイされたモジュール)がこれらの機能をまったく使わない場合は、enabled = false に設定するだけで、インボックスのメモリバッファとバックグラウンドの一括ディスク書き込み IO を節約できます——メッセージの送受信とコマンド処理には一切影響しません。

メッセージ帰属(sender と meta)

すべてのクエリインターフェース(get / recent / get_by_trace / event.history)の各レコードは、 帰属フィールドを統一して持ち、「誰が発言したか / どのモジュールが送信したか」を区別するために使用されます:

フィールド 意味
sender 送信者識別子:インバウンドは user_id、アウトバウンドは bot アカウント / エコー時の self.user_id
meta 記録時点のメタデータ(ホワイトリスト契約、完全な raw event ではない)

meta のホワイトリスト:

旧バージョンの記録(meta カラムが存在しなかった時期のもの)の meta は {} となり、呼び出し元の meta.get("nickname", "") は自然にフォールバックします。ストレージバックエンドが ALTER をサポートしていない場合も、同様にサイレントに {} へフォールバックします。

アウトバウンドの帰属(meta.module)

同一インスタンス上では複数のモジュールがメッセージを送信します(チャット、管理、定期リマインダーなど)。アウトバウンド記録の meta.module は送信の帰属を示し、値の優先順位は current_owner > current_caller > "" です:

送信シナリオ module の値
モジュールコードが module.call 経由で呼び出されている間 対象モジュール名
モジュールのイベントハンドラ内での送信 そのハンドラを登録したモジュール名(ディスパッチ時に自動注入)
フレームワーク自身 / モジュールコンテキストなし ""

message.sent / message.sending フックのペイロード(send_ctx)も同様に module フィールドを持ちます。

インバウンド bot エコー(self_echo)

一部のプラットフォーム/プロトコル実装では、bot 自身が送信したメッセージを message イベントとしてエコーバックして報告します(例:OneBot v11 の report-self-message、Telegram の outgoing message)。フレームワークは受信記録の前に self.user_id == user_id によってエコーを識別し、self_echo の設定に従って処理します:

値 動作
"bot"(デフォルト) 記録は行うが、role="bot"、sender=self_id、meta.self_echo=true とする
"skip" 記録しない(アダプタが同時に message.sent を発火する場合、アウトバウンド記録は自然に存在する)
"user" role="user" として記録(旧動作、互換性のためのエスケープハッチ)

エコーバックしないプラットフォーム(self.user_id != user_id が常に成立する場合)では、この機能を無効にした場合とまったく同じ動作になります。

セッション別フィルタリングの recent

from ErisPulse import transcript

# セッション横断のグローバル時間ウィンドウ(コールドスタート再生セマンティクス、デフォルト)
transcript.recent(3600)

# 単一セッションに限定:サーバー側でフィルタリングし、グローバル取得後にクライアントが一件ずつ session_key を照合するのを回避
transcript.recent(3600, session_key="onebot11:group:123")

session_key=None はセッション横断のセマンティクスを維持します。空文字列 "" はフィルタリングなしと同等です(フォールトトレランス)。

実行時オーバーライド API

実行中に動的に調整したい場合(例:特定のシナリオ向けに一時的に保持期間を延長する):

from ErisPulse import transcript

# オーバーライド(設定より優先;メモリ上のみ有効、再起動後は設定値に戻る)
transcript.set_retention(max_per_session=500, ttl_hours=24 * 30)

# 有効な値とその出所を照会
transcript.get_retention()
# {'max_per_session': 500, 'ttl_hours': 720.0, 'overridden': ['max_per_session', 'ttl_hours']}

# 設定値に基づく動作に戻す
transcript.reset_retention()

リスクと監査:制限を緩和する前に必読

max_per_session / ttl_hours を無効化(または大幅に引き上げ)すると、transcript テーブルはメッセージ量に応じて 無限に増加します。その影響の連鎖:

  1. ディスク容量の枯渇——sqlite の単一ファイルが膨張し続け、ディスクを使い果たすとフレームワークのストレージ層が使用不能になります;
  2. クエリの劣化——get() / recent() / get_by_trace() のスキャンコストがテーブルの増加とともに上昇します;
  3. 下流のメモリ圧迫(OOM の実際の原因)——履歴を丸ごとメモリへ読み込むコンシューマー (履歴を伴う会話復元、パネルによる全件取得、ビジネスコードでの get(session, n) による大きな n の取得)は テーブルの増加とともにメモリを消費し、最悪の場合 OOM kill が発生します。

緩和する前に自問すべき 3 つのこと:どのくらいの期間の履歴が必要か?どれくらいの量が必要か?誰がクリーンアップするのか? 答えが「永遠に削除しない」に依存するなら、正しいアプローチは通常、外部アーカイブ(古いデータを定期的に専用データベースへ移動する)であり、 受信トレイに無限の保持をさせることではありません。

監査手段(テーブル規模の定期チェック):

# ORM/SQL ビルダー経由で行数とサイズを照会(sqlite の例)
from ErisPulse import storage

row = storage.Table("transcript").raw_sql("SELECT COUNT(*) AS n FROM transcript")
-- sqlite ファイルへ直接クエリ(テーブルサイズ)
SELECT COUNT(*) FROM transcript;

保持ポリシーが無効化されました という warning ログを受け取った場合 = 誰かが 0 を渡してポリシーを無効化したことを意味するため、ご自身の操作であることを確認してください。

他のメカニズムとの境界

関連ドキュメント