Cross-Session Protocol
このページは、Qwen Code のセッションではなくて cross-session メッセージングに参加したいプログラムのコントラクトです。音声フロントエンド、リレードーモン、ビルドを監視するスクリプトなどが該当します。セッションがレジストリに何を書くか、inbox が接続から何を読み取るか、そして何を返すかを説明します。ここにあるすべては、schema version 1 および frame version 1 における現在のコードの動作です。最後のセクションで、変更される可能性とその通知方法について説明します。
プロセス境界を越えるすべての値は、到着時に信頼されないものとして扱われ、読み手によって検証されます。このページでフィールドが「〜でなければならない」と書かれている場合、それに従わない値は破棄されます。エラーで拒否されることはありません。
1. セッションレジストリ
実行中のセッションは 1 つのレコードを公開します。
$QWEN_HOME/sessions/<pid>.json (directory 0700, file 0600)
$QWEN_HOME/sessions/<pid>-<8 hex>.json (a process hosting several sessions)$QWEN_HOME のデフォルトは ~/.qwen です。ファイル名は書き手の PID によってキー付けされます — PID そのものか、PID、ダッシュ、登録時に生成された 8 つの小文字 16 進文字のいずれかです(下記の「1 プロセスからの複数のレコード」を参照)。pid フィールドがファイル名の PID プレフィックスと一致しないレコード — 正規の 10 進形式で比較されるため、ゼロパディングされた名前は何とも一致しません — は無視されます。
{
"schemaVersion": 1,
"pid": 41337,
"procStart": "a1b2c3d4-…-boot-uuid:8895124",
"pidNs": 4026531836,
"sessionId": "8e016be8-5b48-4c13-ad22-1f5326ae64ac",
"cwd": "/home/me/project",
"name": "project-3f",
"startedAt": 1788959000000,
"qwenVersion": "0.23.0",
"kind": "tui",
"ipcPath": "/run/user/1000/qwen-socks/41337.sock",
"ipcToken": "c0ffee…64 hex…"
}| フィールド | 意味 |
|---|---|
schemaVersion | 常に 1。読み手はより高いバージョンのレコードをスキップし、削除することはありません。 |
pid | 書き手のプロセス ID。ファイル名がキー付けする PID と等しくなければなりません。bare 形式では名前全体、生成形式では -<8 hex> サフィックスの前の数字です。 |
procStart | Linux では <boot id>:<process start ticks>(/proc/sys/kernel/random/boot_id と /proc/<pid>/stat のフィールド 22)。それ以外では null。PID の再利用や、このホームディレクトリを共有する別のマシンで書き込まれたレコードに対するガードです。 |
pidNs | Linux では /proc/self/ns/pid の inode 番号。それ以外では null。読み手は自身の名前空間からのレコードのみを一覧表示およびスイープします。 |
sessionId | セッションの ID。/clear と /resume は同じ PID の下でこれを交換するため、送信前に毎回レコードを再読み取りしてください。 |
cwd | 登録時の作業ディレクトリ。 |
name | 表示名。cwd のベース名から派生します(Unicode の文字、マーク、数字、.、_、-、最大 32 コードポイント)。それに - と sha256(sessionId) の最初の 2 つの 16 進文字が続きます。ただし、書き手が選択した場合はその値を使用します。一意ではありません。 |
startedAt | エポックミリ秒。新しい順が一覧表示の順序であり、ツイン間のタイブレークです。 |
qwenVersion | フリーテキストまたは null。 |
kind | 登録した種類: tui(ターミナルにいる誰か)、headless、serve、external。小文字 ASCII、数字、ダッシュのみ、最大 16 文字。それ以外は何でも読み取り時に破棄されます。欠如はこのフィールドより古い書き手であることを意味し、tui として読み取られます。一覧表示のためのラベルであり、決して認証情報ではありません。以下を参照。 |
ipcPath | inbox ソケット。バインドされている間のみ存在します。欠如は検出可能だがメッセージ送信不可を意味します。 |
ipcToken | 64 文字の 16 進数。ipcPath への接続が認証ラインで提示するものです。欠如は inbox が認証を要求しないことを意味します(古いビルドのレコード)。 |
レコードは自己報告です。 中のすべてのフィールドはそれが記述するプロセスによって書き込まれたものです。したがって name、cwd、kind は主張であり、読み手が依存できる事実ではありません。送信者が何を行えるかを決定するものはそれらを読み取りません。それは接続が提示するもの(§3)と、受信セッション自身のポリシー(§6)によって決定されます。kind を設定して一覧表示がセッションを正直にグループ化できるようにしてください。しかしそれで何かを得られると期待しないでください。
独自のレコードの書き込み。 見つかりたい外部プロセス — qwen sessions ps で一覧表示される、send_message からアドレス指定できる、レシートを受信できる — は自身のために同じレコードを書き込みます。自身の pid、procStart、pidNs を同じ方法で計算し、自身が生成する sessionId(任意の UUID)、kind: "external"、name(自身のもの、または同じ方法で派生したもの。表示時に 1 行にフラット化され制限されます)、そして自身がバインドする inbox の ipcPath + ipcToken(§2)です。同じディレクトリ内の一時ファイルに書き込み、ターゲットに対して rename してください。ファイルは 0600 で作成し、シンボリックリンクを通じた書き込みを拒否してください。終了時にレコードを削除してください。プロセスがなくなったレコードは次に一覧表示するセッションによってスイープされます。ただし procStart が PID が単に再利用されていないことを証明している場合に限ります。
読み取り。 ディレクトリを読み取れるものはすべて、トークンを含めてすべてのレコードを読み取れます。セッションを発見できることと認証できることは、設計上同じ能力です。ipcToken をモデルやログが見られる場所に表示しないでください。
Liveness. レコードは以下のすべてが成り立つときにライブです。ファイル名が <pid>.json または <pid>-<8 hex>.json であり、その PID プレフィックスが pid と等しい。pidNs が読み手のものと等しい。procStart 内の boot id が読み手のものと等しい(または procStart が null)。そして PID が同じ start ticks で生存している。ipcPath を持つライブレコードは、到達可能として広告される前にダイヤルされる必要があります。ソケットファイルはクラッシュ後も残ります。
Refs。 表示上のハンドルは ref = sha256(sessionId)[0:6] を使用します。2 つのセッションが同じ name を共有する場合があります。送信者が入力するアドレスの文法は name、name [ref]、[ref]、または単独の ref です。曖昧な name は推測ではなくエラーです。
1 プロセスからの複数のレコード。 qwen --acp の子プロセス — デーモンによって spawn されたもの、またはエディタや別のクライアントによって直接駆動されるもの — は、最初のセッション以降、セッションごとに 1 つのレコードを書き込みます。名前は <pid>-<8 hex>.json です。サフィックスは登録時に生成され、変更されることはありません。下でセッション ID が交換されても、それはレコードのパッチであり、リネームではありません。それらはすべて同じ ipcPath を持ちます。なぜなら、プロセスはすべてのセッションに対して 1 つの inbox をバインドし、各フレームの toSessionId で区別するからです。したがって、常に toSessionId を送信してください。それを持たないフレームがそのようなプロセスに到達すると、misaddressed が返されます。なぜなら、それが意図した可能性のある単一のセッションが存在しないからです。Liveness、スイープ、名前空間および boot ガードは、bare 名の場合とまったく同じようにレコードを読み取ります。PID/ファイル名の一致チェックのみが異なり、それは名前全体ではなくサフィックスの前の数字に対して pid を比較する場合のみです。
2. inbox ソケット
セッションごとに 1 つの UNIX ドメインソケット。以下の最初にバインド可能なものを使用します。
$XDG_RUNTIME_DIR/qwen-socks/<pid>.sock$TMPDIR/qwen-socks-<16 hex>/<pid>.sock/tmp/qwen-socks-<16 hex>/<pid>.sock
ディレクトリは 0700、ソケットは 0600 です。パスが 103 バイトを超える場合はスキップされます。PID キーの名前がすでにライブリスナーに保持されている場合(2 つの PID 名前空間がランタイムディレクトリを共有している場合)、セッションは代わりにその隣に <pid>-<8 hex>.sock をバインドします。ピアはソケットパスを派生しません。レコードから ipcPath を読み取ります。
接続は改行区切り JSON を伝送します。1 行に 1 オブジェクト、UTF-8 です。1 MiB を超える行(UTF-16 コード単位で測定)は接続を切断します。30 秒以内に解析可能な行を完成できない接続は切断されます。ジャンク行は期限を延長しません。リスナーは一度に最大 64 接続を受け付けます。
期待される交換は 1 接続あたり 1 メッセージです。接続し、認証ラインとフレームを 1 回の書き込みで書き、半クローズし、ピアがクローズするのを待ちます。受信者は同じ接続では書きません。言うべきことはすべて、あなた自身の ipcPath への別の接続として返されます。
3. 認証ライン
ターゲットレコードに ipcToken がある場合、最初の行は以下でなければなりません。
{ "msgV": 1, "type": "auth", "token": "<token>" }3 種類のトークンが受け付けられ、inbox はどれを見たかを記憶します。
| 提示されるもの | inbox の結論 | 効果 |
|---|---|---|
ターゲットのレジストリレコードからの ipcToken | 通常のピア | ポリシーとモードのパリティに従う(§6) |
ターゲット自身の環境からの QWEN_CODE_MESSAGING_TOKEN | そのセッションが開始したプロセス | パリティのデフォルトの下で配信。origin="own-process" |
qwen sessions controllers add で生成されたコントローラトークン qpc_<64 hex> | ユーザーが信頼するプログラム | パリティのデフォルトの下で配信。origin="controller" とグラントのラベル |
最初の行が認証ラインでない場合、または 3 つのいずれにも一致しないトークンを提示する場合は、接続はサイレントに切断されます。レコードに ipcToken がない場合、認証ラインを送信しないでください。古い inbox はそれを不明なフレームタイプとして読み取り、スキップします。したがって最初に送信しても常に安全です。
ここには_送信者_を認証するものはありません。トークンは接続が許可されていることを証明しますが、誰がオープンしたかは証明しません。from、fromName、fromMode およびレコードのすべてのフィールドは主張です。
これも信頼モデルの全体です。セッションを操作したいプログラムは、コントローラトークンを取得します。これは手動で生成され、そのプログラムにのみ渡されます。これが配信されるメッセージとレビュー待ちのメッセージの違いを生み出します。kind: "external" や親しげな name を書いても何も得られません。
4. ユーザーフレーム
{
"msgV": 1,
"msgId": "5f1d0c9e-3b2a-4e8f-9c7d-1a2b3c4d5e6f",
"type": "user",
"from": "/run/user/1000/qwen-socks/40011.sock",
"replyToken": "<my own ipcToken>",
"fromName": "project-3f",
"fromMode": "prompting",
"toSessionId": "8e016be8-…",
"priority": "next",
"message": { "role": "user", "content": "build finished, 0 failures" }
}| フィールド | ルール |
|---|---|
msgV | 数値。1 以下でなければなりません。より高い値は破棄されます。 |
msgId | ^[A-Za-z0-9][A-Za-z0-9_-]{0,63} で、正規化(ダッシュの除去、小文字化)しても all になってはなりません。メッセージごとに新しい UUID を使用してください。受信者は既に判定した ID を記憶し、再送信されたものには古い判定を返します。 |
type | "user"。 |
from | 自身の ipcPath(存在する場合)。レシートの送信先。欠如はレシートなしを意味します。 |
replyToken | 自身の ipcToken。受信者がレシートを認証するために使用します。 |
fromName | 表示名。1 行にフラット化され、最大 200 文字。 |
fromMode | "prompting"(人が各アクションをレビューする)または "bypass"(一部のアクションはレビューなしで適用される)。欠如は「何も主張しない」を意味し、レビュー待ちとして保持されます(§6)。 |
toSessionId | レコードから読み取った sessionId。異なる ID を保持する受信者は misaddressed を返します。常に送信してください。 |
priority | "now" または "next"。それ以外は何でも "next" として読み取られます。将来のインタラプトパスのために保持されます。現在、受信者は両方を次のターン用にキューに入れます。 |
message | role は "user" でなければなりません。content は空でない文字列です。 |
不明なフィールドは無視されます。
5. delivery-status フレーム
受信者はメッセージの成り行きを 1 つのコントロールフレームで報告します。結果ごとに 1 つ、メッセージの from に送信され、replyToken で認証されます。
{
"msgV": 1,
"msgId": "<fresh id>",
"type": "control",
"action": "delivery_status",
"status": "held",
"origMsgId": "5f1d0c9e-…",
"from": "/run/user/1000/qwen-socks/41337.sock",
"reason": "Your message is held for the recipient user to review …"
}status | 時期 | 対応 |
|---|---|---|
held | ユーザーのレビュー待ち。リトライ時、およびキューに入れられなかったリリース時に繰り返されます。 | 待機してください。決定または期限切れが続きます。 |
delivered | モデル用にキューに入れられました。 | なし。読み取られたことの証明ではありません。 |
denied | 人がレビューして拒否しました。 | 再送信しないでください。 |
refused | セッションのポリシーがピアメッセージを拒否しました。誰も見ていません。最初のレシートのみです。 | 停止してください。別の方法でそのユーザーに連絡してください。 |
expired | 保持されていたメッセージの待機時間が終了した、セッションが未読のまま終了した、またはセッションのシャットダウン中に到着した。held または delivered に続く場合があります。 | まだ重要な場合は後で再送信してください。 |
misaddressed | toSessionId がそのアドレスのセッションと一致しません。 | レジストリを再読み取りしてください。 |
dropped | ポリシーが実行される前に inbox が拒否しました(§6)。 | 未送信として扱ってください。ループでリトライしないでください。重要な内容は後の 1 つメッセージにまとめてください。 |
dropped レシートは 2 つの追加フィールドを持ちます。dropReason は rate-limited、duplicate、queue-full のいずれかです。droppedMsgIds は同じレシートが判定する最大 256 個の追加 ID をリストします。バーストは個別ではなく 1 つのレシートで応答されるため、送信者は失ったメッセージを単一のフレームから終端状態に移動します。両方とも他のステータスでは無意味であり、無視されます。
reason は人間のためのフリーテキストです。レシートの順序は接続間で保証されません。状態遷移として適用してください。
pending → held | delivered | denied | refused | expired | misaddressed | dropped
held → delivered | denied | expired | misaddressed
delivered → expired | misaddressedそれ以外は繰り返しであり、無視する必要があります。送信したことがない ID のレシートはノイズです。無視してください。レシートは受信者側でベストエフォートです。送信制限の超過または from の死亡はそれらをサイレントに失います。したがって送信者は応答を受け取れないことを許容しなければなりません。
あなた自身の inbox は、メッセージを送信したセッションからこれらのフレームを受け取ります。送信のみを行う場合でも、inbox をバインドして from を指定してください。それがないと、上記のすべての結果が見えません。
6. 受信者がメッセージをどう処理するか
順番に:
- 受け入れ(Admission)。 送信者ごと:バースト 30、その後 2 秒ごとに 1 メッセージ。全送信者合計:バースト 32、その後 1 秒に 1 つ。送信者はフレーム上で自身を名前指定するため、その名前をローテーションすると最初の制限から新しい許可が得られますが、2 番目の制限からは得られません。30 秒以内に別のセッションから同じ本文は
duplicateです。セッションが開始したプロセスと信頼されたコントローラはそのチェックから免除されますが、他の全員と同様にレート制限されます。ドロップされたメッセージは保持されず、配信されず、レコードを残しません。したがってバーストを待ってリトライする送信者でも到着します。 - 判定済み ID。 ゲートが既に判定した
msgIdは以前の判定を繰り返します。 - ポリシー。
agents.crossSessionInboundがaccept、hold、refuseに設定されている場合、それが優先されます。未設定の場合:セッションが開始したプロセスまたは信頼されたコントローラは受け入れられます。それ以外の場合、メッセージはfromModeが受信者がいるのと同じレビュークラスを名前指定する場合にのみ受け入れられ、他のすべての場合、fromModeが欠如している場合も含めて保持されます。 - 保持。 最大 50 メッセージが待機します。フルバッファーに到着したメッセージは、既に保持されているものを追い出すのではなく
queue-fullでdroppedされます。保持されたメッセージはagents.crossSessionHeldExpiry(1m、5m、10m、never、デフォルト5m)後に期限切れになります。ユーザーは/peersからリリースまたは拒否します。モードの変更はバックログを再評価します。 - キュー。 受け入れられたメッセージはセッションの入力キューに参加します。ピアからの最大 50 を保持します。フルキューも
queue-fullでdroppedされます。
送信者は制限を苦労して発見する必要はありません。Qwen Code セッションはアドレスごとにそれらをミラーリングし、書き込む前に自身の送信を拒否し、モデルに代わりにバッチ処理するよう指示します。
モデルは配信されたメッセージを以下として表示します。
<cross_session_message from="/run/user/1000/qwen-socks/40011.sock" name="project-3f">
build finished, 0 failures
</cross_session_message>その後、送信者の権限を示す通知が続きます。origin="own-process" または origin="controller" controller="<label>" は接続が提示したものから受信者によって追加されます。フレームからは追加されません。コントローラのラベルは fromName からではなく、ユーザーが生成したグラントから来ます。エンベロープのように見えるタグは content 内で無力化されます。
7. 互換性
- 読み手は知らないフィールドを無視します。レコードまたはフレームにフィールドを追加することは破壊的変更ではありません。
schemaVersionとmsgVは既存フィールドの形状の変更がある場合にのみインクリメントされます。読み手は既知のバージョンを超えるフレームをドロップし、レコードをスキップします。そのようなレコードを削除することはありません。- 新しい
status値が現れる可能性があります。不明なものは「遷移なし」として扱い、待機を続けてください。同じことが認識できないkindにも言えます。表示してください。修正しないでください。 - 予告なく変更される可能性のある定数:バーストおよびレート値、保持上限と期限切れの選択肢、1 MiB 行制限、30 秒行期限、64 接続制限。
8. まだ未確定
- Name yielding。 1 つのディレクトリ内の 2 つのセッションが同じ
nameを登録できます。現在はrefによってのみ区別されます。ライブ名に譲る登録、およびセッションが自身をリネームしたことをピアに伝えるコントロールフレームは、どちらもまだ今後の予定です。 - Same-name reporting。
qwen sessions psとlist_agentsはまだ衝突しているレコードにフラグを立てません。 - ACP 駆動セッションへのインバウンドメッセージ。 プログラムが ACP 経由で駆動するセッション — デーモンによって spawn されたかどうかにかかわらず — は登録され、送信できますが、それ自身に送信されるものに対しては
refusedを返します。hold は人に対する質問であり、その代わりに hold リストを見ている人はいないからです。hold されたメッセージがそれらのセッション(そのクライアント、またはデーモン自身の API)のどこに表面化すべきかは、まだオープンです。 - 1 つの inbox の背後にあるセッションは、すべてのピアに対して 1 つの送信者です。 複数のセッションをホストするプロセスは 1 つの
fromアドレスで送信するため、受信者の送信者ごとのバジェットと重複ウィンドウ(§6)は、そのプロセスのすべてのセッションで共有されます。忙しい兄弟セッションが別のセッションの許可を消費する可能性があり、あるセッションに送信された本文はウィンドウ内ではその兄弟に繰り返すことができません。セッションごとのアカウンティングはフレームで主張されたフィールドを信頼する必要があり、§3 の信頼モデルはそれを排除しています。