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 按需读取:只读需要的章节即可,不必全文加载。
- 影子模块: 31-61 行
- クイックスタート: 63-89 行
- 5つの分離ゲート: 91-101 行
- 誠実な限界(防げないもの): 103-112 行
- 昇格とロールバック: 114-123 行
- 関連ドキュメント: 125-129 行
- 消息追踪: 131-145 行
- 動作原理: 147-176 行
- 設定: 178-195 行
- メッセージ帰属(sender と meta): 197-256 行
- 実行時オーバーライド API: 258-278 行
- リスクと監査:制限を緩和する前に必読: 280-309 行
- 他のメカニズムとの境界: 311-316 行
- 関連ドキュメント: 318-322 行
影子模块
来源:
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 >!--}
- 起動:
await sdk.module.shadow_start("roll", source="path/to/v2")——新バージョンのコードは独立した owner(デフォルトはパス名)として旧バージョンと共存 - シャドウは実際のディスパッチと依存グラフには参加しません:同名コマンドはシャドウのカタログに入り、ルートは登録のみでマウントせず、
ライフサイクルブロードキャストはサイレント、
module.callと依存解決は引き続き v1 を指します - 昇格は常に人間が確認します:
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": [...]}
- v1 の実際の送信:受信箱(transcript)からの bot タイムライン
- v2 の送信意図:シャドウ台帳(アウトバウンドゲートが記録した「送りたかったもの」)
- 両者は
trace_idで整列されます——同じメッセージについて、2つのバージョンがそれぞれなぜ発火した/しなかった、 何を送りたかった/実際に送ったのかが一目瞭然
問題ないことを確認したら昇格:
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 を指し、未完成品が依存されるのを回避) |
設定の継承:シャドウはデフォルトで元モジュールの設定セクションを継承します(そうでないとカナリアの結果が歪みます)。昇格後は設定がそのまま有効になります。
誠実な限界(防げないもの)
- フレームワーク経由の送信 / API / KV ストレージ / 統一 HTTP クライアントはすべて傍受可能;
モジュールがフレームワークを迂回して生の
aiohttpを起動したり、スレッドを立てて外部システムへ書き込むことは——フレームワークでは防げません - ORM の読み書きはオーバーレイのセマンティクス外です(行単位の overlay は SQL レイヤーではクリーンに実装できない)—— シャドウ期間中は ORM 書き込み分離への依存を避けることを推奨
- シャドウソースはローカルパス:新バージョンのコードはパスからインポートされ、独立した owner としてロード;同一 PyPI パッケージは
同一インタプリタ内では
sys.modulesの単一キー制限を受け、新旧2バージョンの共存は不可能 - リーク監査器(
sdk.module.audit)はシャドウリソースの帰属を可視化;フレームワークを迂回した副作用も、 少なくとも無音のままにはなりません
昇格とロールバック
promote の流れ:現在のバージョンをスナップショット(カスケード依存モジュールを含む)→ 完全アンロード → シャドウを本名で登録・
ロード → いずれかのステップが失敗すれば自動ロールバック、旧インスタンスがサービスを継続(ベストエフォートのセマンティクス:on_unload で
既に実行された副作用は取り消し不可、ロールバック後の旧インスタンスは終了処理済みの状態)。昇格成功後、シャドウのリソースは
回収され、バインドは解除;元モジュールの設定セクションがそのまま有効になります。
永続化の注意:promote はランタイムでの切り替えです——再起動後も v2 で動作し続けるには、新バージョンを
永続的にインストールする必要があります(pip install -U で新バージョンを導入 / プラグインファイルを差し替え)。
ランタイムでの切り替えはパッケージ管理を代行しません。
関連ドキュメント
- オーナー(owner)システム——シャドウが独立した owner で分離される仕組みの基盤
- インタラクションセッション——
trace_idと受信箱(diff 整列のデータソース) - スコープ(scope)——イベントの准入とアウトバウンド制御プレーン
消息追踪
来源:
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 のホワイトリスト:
- インバウンド:
nickname(イベント発生時点のニックネーム。後から逆引きで取得できるのは「現在のニックネーム」)、self_id(bot アカウント)、sub_type、message_id。エコーメッセージには追加でself_echo: trueが付与されます。 - アウトバウンド:送信コンテキスト(
platform/method/detail_type/target_id/bot_id/trace_id/preview)とmodule(送信の帰属モジュール、下記参照)。
旧バージョンの記録(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()
0を渡す = 対応するポリシーの無効化(warning ログに記録が残ります)。負の値はValueErrorを送出します;- オーバーライドは次回の保持クリーンアップ時に反映されます(遅延実行のため、最大で約 32 件分の追記サイクル遅延します);
transcriptシングルトンはルートパッケージからインポートできるほか、sdk.transcriptからもアクセスできます。
リスクと監査:制限を緩和する前に必読
max_per_session / ttl_hours を無効化(または大幅に引き上げ)すると、transcript テーブルはメッセージ量に応じて
無限に増加します。その影響の連鎖:
- ディスク容量の枯渇——sqlite の単一ファイルが膨張し続け、ディスクを使い果たすとフレームワークのストレージ層が使用不能になります;
- クエリの劣化——
get()/recent()/get_by_trace()のスキャンコストがテーブルの増加とともに上昇します; - 下流のメモリ圧迫(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 を渡してポリシーを無効化したことを意味するため、ご自身の操作であることを確認してください。
他のメカニズムとの境界
- 対話セッションタイマー / wait_reply:対話セッションシステム を参照してください。受信トレイとは無関係です;
- 会話復元による履歴の持ち帰り:
resume(with_history=N)は受信トレイから直近の N 件を取得します—— 受信トレイが切り詰められると、持ち帰れる履歴もそれに応じて減少します(max_per_sessionを引き上げれば復元可能な期間を延長できます); - シャドウモジュールの diff:
get_by_traceによるトレース照会に依存するため、TTL が短すぎる受信トレイは diff 可能な期間を短縮します。
関連ドキュメント
- 受信トレイの履歴を消費する箇所:会話復元 Conversation、イベントレベルの読み取り event.history、シャドウ diff シャドウモジュールと正式版への昇格
- テーブル規模の監査に使用するストレージインターフェース → ストレージバックエンド と SQL クエリビルダー