关于 session-events
session-events 是面向 Claude Code 会话的轮询守护进程框架,通过源脚本约定监视目标并报告变化。它可将提醒发送到会话收件箱,也可为其他 Agent 写入文件日志;用户还可为其他系统编写源脚本。
主要功能
- 01通过源脚本轮询目标
- 02将提醒作为会话消息发送
- 03记录事件并保存轮询状态
- 04提供 watch、wait、status 和 stop 命令
- 05为没有收件箱的 Agent 提供文件输出
适用场景
- 01Agent 工作期间监视构建日志
- 02轮询队列、部署或工单来源
- 03读取非 Claude Agent 的事件更新
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vivswan/ skills --skill session-events安装后在 Claude Code 中输入/session-events确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
LICENSE.mdREADME.md.codex-plugin/plugin.jsonagents/openai.yamlscripts/inbox.mtssession-events.mtsstate.mtsscripts/examples/log-source.mts详细介绍
会话事件
每个 Claude Code 会话启动一个守护进程作为后台任务,轮询会话所监视目标所在的系统,将每轮轮询中的警报作为一条消息发送,并将每个事件记录为一行,供任意 Agent 读取。轮询内容由一个源定义:一段几行的脚本,说明目标是什么、如何读取,以及事件内容是什么。
本目录提供三个源:/watch-github 技能轮询 GitHub 上的 PR 和分支最新提交,/orchestrator-mode 技能的 fleet-events 轮询一组 Agent 的对话记录和工作树,scripts/examples/log-source.mts 监视文件大小。对于其他需要轮询的内容,会话可以自行编写源。
运行范围:一个 Claude Code 会话、一位用户、由该会话启动一次的守护进程,以及可访问的被轮询系统。
示例
一个 Agent 正在等待构建任务写入 /var/log/build.log。它每个会话在文件源上启动一次守护进程,将其作为后台任务运行,并通过一行代码订阅该文件:
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
会话继续工作。构建任务追加一行内容后,下一轮轮询会将此帧发送到会话的收件箱,并将其呈现为来自 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,并在顶层传入一个对象调用它;runEvents 会读取进程的参数并运行该源的整个 CLI。包含四个必填字段和三个可选字段:
| 字段 | 源文件的说明 | 守护进程的用途 |
|---|---|---|
name | "log-events"([a-z0-9-]{1,40}) | 文件名主干、信封的 from-name、msg_id 前缀、结束行 |
interval | 5 | 忙碌时轮询之间的秒数,以及 --interval 的默认值;下限为 1,上限为 86400 |
idleInterval(可选) | 未设置,因此为 5 | 两次轮询之间的最长等待时间;无事发生的轮询会将等待时间从 interval 开始加倍,直至此值 |
usage(可选) | 单行:目标是绝对文件路径 | --help 末尾显示的行:目标格式和源自身的标志 |
parseTarget(args) | 路径;若非绝对路径则拒绝 | watch <target> [flags]:watch 后的参数,先是目标文本;抛出 UsageError,退出码为 2 |
poll(targets, io) | 对每个文件执行 stat,并与保存的大小比较 | 每次轮询:事件,以及是否仍有内容在变化 |
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 显示,但不会发送),这样源就能将问题作为警报提示,同时以备注形式展示其余信息。
轮询必须遵守以下规则:
- 返回结果,不进行流式传输。 事件会以一个数组的形式返回;守护进程为它们编号并添加时间戳,将其追加到日志,保存状态,然后发送。
- 返回前写入游标。 抛出异常的轮询属于失败轮询:守护进程会记录原因且不保存任何内容,因此,若源在抛出异常前移动了游标,就会跳过游标越过的内容。
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 也算正常,第二次启动 daemon 也一样,它会提示第一个实例正在运行 |
| 2 | 用法错误(未设置 CLAUDE_CODE_MESSAGING_SOCKET 且未指定 --id、--id 无效、指定了 --pid 但未指定 --id、--interval 超出范围、目标被源拒绝)、状态文件不是 JSON,或没有运行时目录 |
| 3 | 此会话没有守护进程响应:stderr 会显示套接字,以及启动守护进程的准确后台命令 |
| 4 | 仅限 wait:超时已到,但没有新行 |
watch 会保存目标并立即轮询,因此会在该次轮询后返回。再次对同一文本执行 watch 会替换目标,包括其游标。poll 会立即运行一次轮询,用于测试和调试。--interval 会调整忙碌间隔;空闲间隔不会低于它。--mode 是会话的权限模式(见下文),默认为 bypass。
信封和文本
发往会话收件箱的帧是 user 角色文本:与所有者输入内容的渠道相同,没有数据渠道。由此有两条规则。
- 信封标明发送者及其模式。 守护进程会将每条消息包装在
<cross-session-message from="uds:<socket>" from-name="<source name>" from-mode="<mode>">...</cross-session-message>中,帧上也带有from_mode;与其他帧格式相比,这是客户端会渲染为带名称发送者的格式。属性值中的&、"、<和>会进行转义。 - 模式是会话的权限模式。 绕过权限提示的会话使用
bypass(默认值);会询问权限的会话使用prompting。接收方持有的帧,其模式与自身不匹配,因此启动守护进程时要使用它所服务的会话模式。 - 文本中不会引用轮询系统的任何内容。 消息中的评论正文或日志行会被看作是所有者写的,因此来源端不会放入这类内容:它只说明发生了什么以及位置(标识符、计数、状态、URL)。如果某个标识符包含闭合标签的拼写形式,就在其方括号前加一个空格;正文中的其他内容一概不改写。
守护进程是会话自身的子进程,因此它发送的帧无需权限审批,会话结束时它也会自行退出。如果以分离方式启动(nohup、setsid、disown、退出的 shell),它就会成为跨会话发送者,每条消息都会被暂存(见下文“注意事项”)。
一次轮询产生的所有警报会合并为一条消息,且与上一条消息至少间隔 1.1 s,因为接收方会丢弃同一发送者在 1 s 内发送的第二帧;相关说明则保留在日志中。各条警报之间以空行分隔,每条警报包含其文本和 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
两小时上限。 Claude Code 会在两小时后结束后台任务,这是它允许的最长时长;此时日志会以 daemon <pid> stopped: SIGTERM 结尾,而会话仍在运行。使用相同命令重新启动守护进程:已保存的目标会继续运行,重启后会立即轮询,事件编号也会延续。
崩溃。 状态保存前会先追加日志,因此,如果守护进程在两者之间被终止,重启时游标会落后一次轮询,并再次读取该轮询:最多只有一次轮询会重复发送,不会丢失任何内容。如果守护进程在发送前被终止,事件会留在日志中,可通过 status 和 wait 读取。
文件
运行时目录按以下顺序尝试。每个目录都必须可创建为私有目录(模式 0700,归用户所有),否则尝试下一个;若一个都不可用,则以状态码 2 退出并要求设置 SESSION_EVENTS_DIR。
| 尝试项 | 条件 |
|---|---|
$SESSION_EVENTS_DIR | 已设置,且为绝对路径 |
$XDG_RUNTIME_DIR/session-events | 已设置 XDG_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 | 事件日志,每个事件一行;在状态保存前追加 |
<pid>-<start>.<source>.state.json | 带游标的目标及事件计数器,以临时文件写入后重命名的方式保存 |
<pid>-<start>.<source>.log | 守护进程日志;出现异常时首先查看此文件 |
<start> 是会话的启动令牌(其在 /proc 中的进程启动时间,或者在没有 /proc 时使用其收件箱套接字的 inode)。后来启动的会话即使碰巧获得相同的 pid,也会打开其他文件,并从空状态开始;系统不会比较所有者。文件接收器守护进程(见下文)的文件名则使用 <id>.<source>.。
没有 Claude Code 收件箱时
Codex、Copilot CLI、shell,或未设置 CLAUDE_CODE_MESSAGING_SOCKET 的 Claude Code 会话,会使用 --id 命名一个文件接收器守护进程,并读取日志,而不是接收消息。将守护进程作为该 Agent 自己的后台任务启动,然后阻塞等待 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>,让它随该进程一起结束:传入 Agent 自己的 pid,绝不要传入一次性 shell 中的 $$;shell 执行守护进程时,$$ 会变成守护进程自己的 pid(守护进程会拒绝自己的 pid)。
wait 会在前台阻塞,等待时间不超过工具自身的上限,并在以退出码 0 结束时打印新增行和 cursor: <id>;将该 cursor 作为下一个 --since 的值传入。退出码 4 表示超时且没有新内容:重新设定等待。退出码 3 表示守护进程已消失:重新启动,然后重新设定等待。
在一组 Agent 中,负责协调的主 Agent 会在自己的会话中将守护进程作为后台任务运行,并设置与会话相匹配的 --mode,然后代表所有人监听;构建 Agent 不会在那里运行 watch。主 Agent 想让某个构建 Agent 执行操作时,会通过一条消息转发相应内容。文件接收器仅供非 Claude Agent 使用。
注意事项
| 看到的情况 | 含义 | 处理方法 |
|---|---|---|
| 会话中出现暂存的送达通知,而不是事件文本 | 守护进程以分离模式运行(nohup、setsid、已退出的 shell),因此它是跨会话发送方 | 执行 stop,然后将其作为本会话的后台任务重新启动 |
watch 以退出码 3 退出,并显示一个 socket 和一条命令 | 此会话和来源没有守护进程 | 将打印出的命令作为后台任务运行,然后再次运行 watch |
退出码 2:未设置 CLAUDE_CODE_MESSAGING_SOCKET | 不是 Claude Code 会话,或该会话没有收件箱 | 启动带 --id 的文件接收器守护进程,并运行 wait(见上文) |
trouble 事件中提到了守护进程日志 | 连续三次轮询失败;守护进程会继续轮询 | 读取日志;修复日志指出的问题(被轮询的系统、凭据、网络) |
日志在启动约两小时后以 daemon <pid> stopped: SIGTERM 结尾,但会话仍在运行 | 后台任务受运行环境两小时上限限制 | 使用相同命令重新启动守护进程;已保存的目标会继续处理 |
| 一条消息收到两次 | 守护进程在追加日志后、保存状态前被终止,重启时再次读取了该次轮询 | 无需处理;最多只会重复一次轮询 |
使用 Agent 工具启动的本地子 Agent 会共享其会话的收件箱 socket 和 pid(CLAUDE_CODE_MESSAGING_SOCKET 和 CLAUDE_PID 属于该会话,并设置了 CLAUDE_CODE_CHILD_SESSION=1),因此在其中运行的 watch 会连接到该会话的守护进程,事件也会发送到该会话。来自 Agent Teams 的队友是拥有独立 socket 的独立进程,并会运行自己的守护进程。