Agent Session SSE

为 Eyrie daemon 建起完整的 agent 事件流系统——从 agent 进程到 Electron 客户端的实时推送管线。

这个 PR 做了什么?

Agent(Claude Code、Codex 等)在工作时会不断产出事件——正在输出的消息、调用了什么工具、需要用户审批、运行是否完成。 之前 daemon 没有任何机制把这些事件传给前端。这个 PR 从零搭建了整个事件流管线—— agent 产出事件 → 存库 → 通过 SSE 实时推给 Electron,同时支持断线重连和崩溃恢复。

📡

实时推送

Agent 的每一条产出——消息、工具调用、审批请求——通过 SSE 实时到达 Electron 客户端

🔄

断线不丢

事件先存数据库再推送,断线重连时从断点自动续传

💥

崩溃自愈

Agent 进程意外退出 → 自动标记状态、通知前端;daemon 重启 → 历史事件完好

一条事件怎么从 Agent 到达 Electron?

1

Agent 进程产出事件

Claude Code / Codex 等 agent 通过 provider adapter 发出原始事件(消息片段、工具调用、运行完成等)

2

SessionSink 接收并编码

event-codec 把原始事件翻译成数据库行——提取索引字段(role、toolCallId 等)、生成状态变更指令(projections)

3

原子写入数据库

同一个 SQLite 事务里完成:插入事件行 → 分配序号(seq / sessionSeq)→ 执行状态投影(run 完成、session 回到 idle 等)

4

内存广播

写入成功后,broadcaster 把事件推给所有订阅了该 session 的 SSE 连接——纯内存,零延迟

5

Hono SSE 推送到 Electron

通过 Hono 的 streamSSE 把事件以 SSE 格式写到 HTTP 响应流,Electron 客户端实时接收并渲染

先落盘再广播:步骤 3 完成后才执行步骤 4。即使 daemon 在广播之后立刻崩溃,事件已经在数据库里——重连时可以从 DB 补回来。

断线重连怎么工作?

正常连接时

Electron收到事件 #1 ~ #100
SSE 连接推送 sessionSeq 1~100
Daemonbroadcaster 实时推

断线后重连

Electron带 Last-Event-ID: 100
Daemon 收到请求 ① 立刻订阅 broadcaster(新事件开始排队)
② 从 DB 查 sessionSeq > 100 的历史
③ 推完历史,接着推实时
Electron收到 #101 ~ 最新,不丢不重

三组状态机驱动整个生命周期

所有状态转换用 CAS(Compare-And-Swap)乐观锁——并发操作谁先到谁赢,不阻塞不报错。

Session 状态

idle active idle
suspended error closed

Run 状态

running completed
failed interrupted cancelled

Approval 状态

pending resolving approved
denied send_failed

Agent 进程的生与死

RunnerManager 负责 agent 进程的创建、复用和善后。

🟢 正常流程

  • 请求到来 → 复用已有 runner,或创建新的
  • 并发请求同一 session → 第二个等第一个创建完
  • 主动关闭 → dispose,等清理完成后返回
  • Daemon 关机 → disposeAll,逐个优雅关闭

💥 崩溃善后

  • Agent 进程意外退出 → 自动检测
  • 当前 run 标记为 interrupted
  • Session 标记为 error
  • 关闭该 session 的所有 SSE 订阅

有了事件流之后能做什么?

💬

Session 页面

实时展示 agent 的消息流、工具调用、审批请求

📝

Diff Review

回放 agent 的代码变更历史,在 UI 内做行内审查

🤖

多 Agent 并行

每个 agent 独立的事件通道,互不干扰