mcpField
S
session-events

session-events

github.com/vivswanエージェントワークフロー119 回インストール2 日前に更新
リポジトリ

session-events について

session-events は、Claude Code セッション向けのポーリングデーモンフレームワークです。ソーススクリプトの仕様に沿って対象を監視し、変化を通知します。通知はセッションの受信箱に届けるほか、他のエージェント向けにファイルへ記録でき、別のシステム用ソースも作成できます。

主な機能

  1. 01ソーススクリプトによる対象のポーリング
  2. 02セッションメッセージとしてのアラート配信
  3. 03イベント記録とポーリング状態の保存
  4. 04watch、wait、status、stop コマンド
  5. 05受信箱のないエージェント向けファイル出力

活用シーン

  1. 01エージェント作業中のビルドログ監視
  2. 02キュー、デプロイ、チケットのポーリング
  3. 03Claude 以外のエージェントでイベント更新を確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/vivswan/skills --skill session-events

インストール後、Claude Code で/session-eventsを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

LICENSE.md
README.md
.codex-plugin/
plugin.json
agents/
openai.yaml
scripts/
inbox.mts
session-events.mts
state.mts
scripts/examples/
log-source.mts

詳細

セッションイベント

1 つの Claude Code セッションにつきデーモンを 1 つ起動します。セッションがバックグラウンドタスクとして起動し、そのセッションが監視する対象をシステムに問い合わせ、各ポーリングのアラートを 1 つのメッセージとして配信し、すべてのイベントをあらゆるエージェントが読める 1 行として記録します。ポーリングする対象はソースです。対象とは何か、どのように読み取るか、イベントにどのような内容を記述するかを数行で示すスクリプトです。

このカタログには 3 つのソースが含まれています。/watch-github スキルは GitHub の PR とブランチの先端をポーリングし、/orchestrator-mode スキルの fleet-events はフリート内のエージェントのトランスクリプトとワークツリーをポーリングし、scripts/examples/log-source.mts はファイルのサイズを監視します。それ以外にポーリングが必要なものについては、セッションが独自のソースを作成します。

動作条件:Claude Code セッション 1 つ、ユーザー 1 人、そのセッションが 1 度起動するデーモン、ポーリング対象のシステムに接続可能であること。

具体例

エージェントが /var/log/build.log に書き込まれるビルドを待っています。セッションごとに 1 度、ファイルソースを使ってデーモンをバックグラウンドタスクとして起動し、1 行でファイルを購読します。

bun "<skill-dir>/scripts/examples/log-source.mts" daemon --interval 5   # once per session, as a BACKGROUND task
bun "<skill-dir>/scripts/examples/log-source.mts" watch /var/log/build.log

セッションは作業に戻ります。ビルドが 1 行を追記すると、次のポーリングでこのフレームがセッションの受信トレイに届き、log-events からのメッセージとして表示されます。

<cross-session-message from="uds:/run/user/1000/session-events/4242.log-events.sock" from-name="log-events" from-mode="bypass">
/var/log/build.log: 0 to 1097 bytes
file:///var/log/build.log
automated notice from log-events; no reply needed
</cross-session-message>

「まだ何も変わっていない」という理由でターンを消費することはありません。ソースが種別、テキスト、URL を記述します。残りはデーモンが記述します。送信者を示すエンベロープ、終了行、ログ行、そして次のポーリングで再開するための保存状態です。ソース全体は scripts/examples/log-source.mts です。

import { statSync } from "node:fs";
import { runEvents, UsageError } from "../session-events.mts";

/** The cursor: the size at the last poll, null before the file has been seen or while it is gone. */
type File = { path: string; size: number | null };

runEvents<File>({
  name: "log-events",
  interval: 5,
  usage: ["targets: the absolute path of a file; an event when its size changes or it disappears"],
  parseTarget([path, ...rest]) {
    if (path === undefined || !path.startsWith("/"))
      throw new UsageError("watch needs an absolute path");
    if (rest.length > 0) throw new UsageError(`unknown option: ${rest[0]}`);
    return { path, size: null };
  },
  async poll(targets) {
    const events = [];
    for (const file of Object.values(targets)) {
      const size = statSync(file.path, { throwIfNoEntry: false })?.size ?? null;
      const url = `file://${file.path}`;
      if (size === null && file.size !== null) {
        events.push({ kind: "gone", text: `${file.path}: gone`, url });
      } else if (size !== null && file.size !== null && size !== file.size) {
        events.push({ kind: "changed", text: `${file.path}: ${file.size} to ${size} bytes`, url });
      }
      file.size = size;
    }
    return { events, busy: events.length > 0 };
  },
});

適用する場面

  • セッションが、ビルドログ、キュー、デプロイ、チケットなど、セッションの外部にありポーリングが必要なものを待っている場合。デーモンがポーリングし、セッションは作業を続けながらメッセージを読み取ります。
  • PR、ブランチの先端、CI の場合:このデーモンのソースであり、専用ページがある /watch-github スキルを使います。ここではなく、そちらから始めてください。
  • ソースを作成する場合:以下の仕様を参照してください。ファイルソースを実例として説明します。

ソースの契約

ソーススクリプトは scripts/session-events.mts から runEvents をインポートし、オブジェクトを1つ渡してトップレベルで呼び出します。runEvents はプロセスの引数を読み取り、そのソースの CLI 全体を実行します。4つの必須フィールドと3つの任意フィールドがあります。

フィールドファイルソースでの指定内容デーモンでの用途
name"log-events"([a-z0-9-]{1,40})ファイル名のベース名、エンベロープの from-name、msg_id のプレフィックス、終了行
interval5ビジー状態のポーリング間隔(秒)、および --interval のデフォルト値。最小値は1、最大値は86400
idleInterval(任意)未設定のため 5ポーリング間の最長待機時間。静かなポーリングでは、待機時間が interval からここまで倍増します
usage(任意)1行:対象は絶対ファイルパス--help の末尾に表示される行。対象の形式とソース独自のフラグ
parseTarget(args)パス。絶対パスでなければ拒否されますwatch <target> [flags]:watch の後に続く引数。対象のテキストが先頭です。終了コード2の UsageError をスローします
poll(targets, io)各ファイルの状態を取得し、保存済みのサイズと比較します各ポーリングのイベントと、変化が続いているかどうか
summary(target)(任意)未設定のため、対象の行だけが表示されますstatus:各対象の下に表示される行。現時点の状態を表します(アラートがあればそれぞれ表示)

デーモンは targets を保持します。これはユーザーが入力したテキストを、parseTarget が返したオブジェクトに対応付けるマップで、各ポーリング後に保存されます。ソースは各対象オブジェクトにカーソルを書き込みます。ファイルソースでは、最後に確認したサイズをそこに保持します。デーモンが状態に追加するのはイベントカウンターだけです。

イベントは { kind, text, url } と、ソースが追加する任意のフィールドで構成されます。デーモンは id と at を先頭に追加します。ログ行は {"id":17,"at":"2026-10-12T14:03:20Z","kind":"changed","text":"/var/log/build.log: 0 to 1097 bytes","url":"file:///var/log/build.log"} です。

デーモンが読み取る追加フィールドは level です。値は alert(デフォルト。メッセージとして配信されます)または note(番号が付けられ、ログに記録され、カウントされ、wait と status に表示されますが、配信はされません)です。これにより、ソースは問題をアラートとして伝え、それ以外の内容をノートとして読みやすく保てます。

ポーリングでは、次のルールを守ります。

  • 戻り値を返し、ストリームは送りません。 イベントは1つの配列で返されます。デーモンは番号とタイムスタンプを付けてログに追加し、状態を保存してから配信します。
  • 戻るまでにカーソルを書き込みます。 例外をスローしたポーリングは失敗です。デーモンは理由をログに記録し、何も保存しません。そのため、スローする前にカーソルを進めたソースでは、カーソルが通過した内容を見落とすことになります。
  • busy は次のポーリング間隔を示します。 保留中のものがある場合、またはイベントが発生した直後は true となり、次のポーリングは interval 後に行われます。false の場合、待機時間は idleInterval まで倍増します。
  • テキストにはソース独自の表現を使います。 識別子、件数、状態、URLなどに、ポーリング対象のシステムから引用した行を使ってはいけません(下記参照)。

コマンド

すべてのソースで同じコマンドを実行します。ファイルソースの --help(このページの他の箇所にある <source.mts> は、独自のソーススクリプトを指します)。

usage: log-source.mts <command> [options]
  daemon [--interval <s>] [--mode bypass|prompting] [--id <name> [--pid <pid>]]
  watch <target> [the source's flags] [--id <name>]
  unwatch <target> | --all [--id <name>]
  wait [--since <id>] [--timeout <s>] [--id <name>]
  poll | status | stop [--id <name>]
the daemon serves the Claude Code session named by CLAUDE_CODE_MESSAGING_SOCKET and exits with it;
--id <name> names a file-sink daemon for a non-Claude agent instead, --pid <pid> ends it with that process;
--mode is the session's permission mode, carried in the envelope: bypass (the default) or prompting;
--interval is 5 s by default, 1 to 86400
targets: the absolute path of a file; an event when its size changes or it disappears
files: <dir>/<pid>-<start>.log-events.{state.json,events.jsonl,log} and <dir>/<pid>.log-events.sock for a session; <id>.log-events.* for a file sink;
  <dir> is $SESSION_EVENTS_DIR, else $XDG_RUNTIME_DIR/session-events, else $CLAUDE_CODE_TMPDIR/session-events-<uid>, else <os tmpdir>/session-events-<uid>, else /tmp/session-events-<uid>
prints: one message per poll with alerts, wrapped in <cross-session-message from="uds:<socket>" from-name="log-events" from-mode="<mode>"> and ending "automated notice from log-events; no reply needed";
  wait prints the new events as JSON lines, then cursor: <id>
exits: 0 ok (stop with no daemon running, and a second daemon start, are ok too); 2 usage, a state file that is not JSON, or no runtime directory;
       3 no daemon answers for this session: stderr names the socket and the background command that starts one; 4 wait only: the timeout passed with no newer line
終了コード意味
0正常です。デーモンが実行されていない状態での stop も正常です。また、2回目の daemon 起動も正常で、最初のデーモンが起動中であることを示します
2使用方法の誤り(CLAUDE_CODE_MESSAGING_SOCKET が未設定で --id もない、--id が不正、--id なしで --pid を指定、--interval が許容範囲外、ソースが拒否する対象)、JSON ではない状態ファイル、またはランタイムディレクトリがない
3このセッションに応答するデーモンがありません。stderr にソケットと、デーモンをバックグラウンドで起動する正確なコマンドが表示されます
4wait のみ:タイムアウトまでに新しい行がありませんでした

watch は対象を保存してすぐにポーリングするため、そのポーリング後に戻ります。同じテキストで2回目の watch を実行すると、カーソルも含めて対象が置き換えられます。poll はテストやデバッグ用に、ポーリングを1回実行します。--interval はビジー状態の間隔を変更します。アイドル時の間隔がこれより短くなることはありません。--mode はセッションの権限モードです(下記参照)。デフォルトは bypass です。

エンベロープとテキスト

セッションの受信トレイに届くフレームはユーザーロールのテキストです。オーナーが入力するものと同じチャネルであり、データチャネルはありません。ここから次の2つのルールが導かれます。

  • エンベロープには送信者とそのモードが記されます。 デーモンはすべてのメッセージを <cross-session-message from="uds:<socket>" from-name="<source name>" from-mode="<mode>"> ... </cross-session-message> で囲み、フレームにも from_mode を設定します。他のフレーム形式と比べて、クライアントが名前付きの送信者として表示するのはこの形式だけです。属性値では &、"、<、> がエスケープされます。
  • モードはセッションの権限モードです。 権限確認プロンプトをバイパスするセッションは bypass(デフォルト)、確認を求めるセッションは prompting です。受信側には自身のモードと一致しないモードのフレームが届くため、デーモンは処理対象のセッションと同じモードで起動してください。
  • テキストにはポーリングしたシステムから引用した内容を含めません。 メッセージ内のコメント本文やログ行はオーナーが書いた内容のように見えるため、送信元がそれらを含めることはありません。何がどこへ移動したか(識別子、件数、状態、URL)を伝えます。識別子が終了タグと同じ文字列になる場合は、その山かっこの前にスペースを入れます。本文のその他の部分は書き換えません。

デーモンはセッション自身の子プロセスであるため、フレームは権限確認による保留なしに配信され、セッションの終了時にデーモンも自動的に終了します。デタッチして起動すると(nohup、setsid、disown、または終了するシェルから起動した場合)、デーモンはセッションをまたぐ送信元となり、すべてのメッセージが保留されます(下記の注意事項を参照)。

1回のポーリングで発生したアラートは1つのメッセージにまとめ、前のメッセージから少なくとも1.1秒空けます。受信側は1秒以内に同じ送信元から届いた2つ目のフレームを破棄し、通知はログに残るためです。アラートは空行で区切り、それぞれにテキストとURLを記載します。メッセージの末尾は automated notice from <name>; no reply needed とします。

ライフサイクル

start     the session runs `daemon` as a background task; the owner is the session whose inbox
          CLAUDE_CODE_MESSAGING_SOCKET names
one only  binding `<runtime-dir>/<pid>.<source>.sock` is the rule: a second start finds the first
          answering and exits 0; a socket file nobody answers on was left by a crash and is removed
          once; two sources of one session are two daemons
poll      every `interval` seconds while busy; each quiet poll doubles the wait, up to `idleInterval`;
          at once on `watch`, and on a restart with saved targets; each poll that returns is a log
          line with its event count and the next wait
trouble   a failed poll is a log line; three in a row are one event, `<name>: the last 3 polls
          failed; read the daemon log for the reason`, with the log's path as its URL, and nothing
          more until a poll succeeds and three fail again
stop      the owner's pid gone (checked every second and before each poll): finish the poll in
          flight, save, unlink the socket, exit; `stop`, SIGTERM, SIGINT: the same at once

2時間の上限。 Claude Code はバックグラウンドタスクを2時間後に終了します。これは許可されている最長時間です。このとき、セッションがまだ実行中でも、ログには daemon <pid> stopped: SIGTERM と記録されます。同じコマンドでデーモンを再起動してください。保存された対象を引き継ぎ、再起動時にすぐポーリングし、イベントの番号も連続します。

クラッシュ。 状態を保存する前にログへ追記するため、その間にデーモンが強制終了されると、カーソルが1回のポーリング分だけ古い状態から再起動し、そのポーリングをもう一度読み込みます。重複して配信されるのは最大で1回のポーリング分で、イベントが失われることはありません。配信前にデーモンが強制終了されても、イベントはログに残り、status と wait から読み取れます。

ファイル

ランタイムディレクトリは次の順に試されます。各ディレクトリは、プライベートディレクトリ(モード 0700、ユーザー所有)として作成できる必要があります。すべて使用できない場合は終了コード 2 で終了し、SESSION_EVENTS_DIR の設定を求めます。

試す順序条件
$SESSION_EVENTS_DIR設定されている場合。絶対パスとして使用
$XDG_RUNTIME_DIR/session-eventsXDG_RUNTIME_DIR が設定されている場合
$CLAUDE_CODE_TMPDIR/session-events-<uid>CLAUDE_CODE_TMPDIR が設定されている場合
<os tmpdir>/session-events-<uid>OS の一時ディレクトリ。TMPDIR を尊重
/tmp/session-events-<uid>最後の手段
ファイル内容
<pid>.<source>.sock制御ソケット。デーモンの from アドレスとしても使用
<pid>-<start>.<source>.events.jsonlイベントごとに1行のイベントログ。状態の保存前に追記
<pid>-<start>.<source>.state.jsonカーソルとイベントカウンターを含む対象。仮ファイルへの書き込み後に名前を変更
<pid>-<start>.<source>.logデーモンのログ。問題が起きたとき、最初に確認するファイル

<start> はセッションの開始トークンです(/proc がある場合はプロセスの開始時刻、ない場合は受信トレイのソケットの inode)。後から起動したセッションがたまたま同じ pid を取得した場合は、別のファイルを開き、空の状態から開始します。所有者の照合は行いません。ファイルシンクデーモン(下記参照)では、ファイル名は <id>.<source>. で始まります。

Claude Code の受信箱がない場合

Codex、Copilot CLI、シェル、または CLAUDE_CODE_MESSAGING_SOCKET のない Claude Code セッションでは、--id を指定してファイルシンクデーモンを起動し、メッセージを受信する代わりにログを読み取ります。そのエージェント自身のバックグラウンドジョブとしてデーモンを起動し、その後 wait で待機します。

bun "<source.mts>" daemon --id codex-12
bun "<source.mts>" watch <target> --id codex-12
bun "<source.mts>" wait --id codex-12 --since 0 --timeout 540

ファイルシンクデーモンは stop するまで動作します。代わりに --pid <pid> を指定すると、そのプロセスが終了したときにデーモンも終了します。エージェント自身の PID を渡してください。ワンショットシェルで $$ を使ってはいけません。シェルがデーモンを実行すると、その PID はデーモン自身の PID になり、デーモンは自身の PID を拒否します。

wait はツール独自の上限より短いタイムアウトを指定してフォアグラウンドで待機し、終了コード 0 で新しい行と cursor: <id> を出力します。次の --since にはそのカーソルを渡してください。終了コード 4 は新しい内容がないままタイムアウトしたことを示します。再び待機状態にしてください。終了コード 3 はデーモンが終了したことを示します。デーモンを再起動してから、再び待機状態にしてください。

フリートでは、リードのセッションが自身のバックグラウンドタスクとしてデーモンを実行し、セッションに合った --mode を指定して、全員に代わって監視します。ビルダーはそこで watch を実行しません。リードは、ビルダーに作業させたいとき、1件のメッセージでそのビルダーに行を転送します。ファイルシンクは Claude 以外のエージェント専用です。

注意点

表示される内容意味対処方法
イベントテキストではなく、セッション内に配信保留の通知が表示されるデーモンが切り離されて実行されています(nohup、setsid、終了済みのシェルなど)。別のセッションから送信されていますstop してから、このセッションのバックグラウンドタスクとして再起動します
watch がソケットとコマンドを示して終了コード 3 で終了するこのセッションとソースに対応するデーモンがありません表示されたコマンドをバックグラウンドタスクとして実行し、その後もう一度 watch します
終了コード 2:CLAUDE_CODE_MESSAGING_SOCKET が設定されていないClaude Code セッションではないか、受信箱のないセッションです--id を指定したファイルシンクデーモンを使い、wait します(上記参照)
デーモンログを示す trouble イベントがあるポーリングに3回連続で失敗しました。デーモンはポーリングを続けていますログを読み、そこに記載されている対象(ポーリング対象のシステム、認証情報、ネットワーク)を修正します
セッションがまだ実行中なのに、開始から約2時間後にログが daemon <pid> stopped: SIGTERM で終わっているバックグラウンドタスクに対するハーネスの2時間の上限です同じコマンドでデーモンを再起動します。保存されたターゲットは引き続き処理されます
同じメッセージが2回届くログへの追記後、状態を保存する前にデーモンが終了し、再起動後に同じポーリングを再度読み取りました対処は不要です。同じポーリングが繰り返されるのは最大1回です

Agent ツールで起動したローカルサブエージェントは、セッションの受信箱ソケットと PID を共有します(CLAUDE_CODE_MESSAGING_SOCKET と CLAUDE_PID はセッションのもので、CLAUDE_CODE_CHILD_SESSION=1 が設定されます)。そのため、サブエージェント内で実行した watch はセッションのデーモンに届き、イベントはセッションに配信されます。agent teams のチームメイトは独立したプロセスであり、独自のソケットを持ち、自分自身のデーモンを実行します。

コメント