mcpField
S
session-events

session-events

github.com/vivswanAgent 工作流119 次安装2 天前更新
代码仓库

关于 session-events

session-events 是面向 Claude Code 会话的轮询守护进程框架,通过源脚本约定监视目标并报告变化。它可将提醒发送到会话收件箱,也可为其他 Agent 写入文件日志;用户还可为其他系统编写源脚本。

主要功能

  1. 01通过源脚本轮询目标
  2. 02将提醒作为会话消息发送
  3. 03记录事件并保存轮询状态
  4. 04提供 watch、wait、status 和 stop 命令
  5. 05为没有收件箱的 Agent 提供文件输出

适用场景

  1. 01Agent 工作期间监视构建日志
  2. 02轮询队列、部署或工单来源
  3. 03读取非 Claude Agent 的事件更新

安装

复制下面的命令,在终端中运行。

$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

详细介绍

会话事件

每个 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 前缀、结束行
interval5忙碌时轮询之间的秒数,以及 --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 的独立进程,并会运行自己的守护进程。

评论

支持 Markdown