@mcd0luo/celes-center-session-bridge
v0.2.8
Published
dsh: cross-session messaging for the Celes dispatch center — address another session by id or name, deliver as its next user turn; queued bridge messages are merged in place (formatted append) instead of piling up turns
Maintainers
Readme
@mcd0luo/celes-center-session-bridge
DSH 调度中心的跨会话通信插件(Celes 系列),同时提供 空闲 agent 释放(W287 内存治本):
包装 agents.resume 收口登记所有 resume 路径(bridge 投递即唤醒 / watchdog session.prompt /
api-session-controller resolveOrResume)返回的 AgentHandle,agent 转 idle 后 TTL 到期经官方
AgentHandle.dispose() 卸载(会话日志保留磁盘,可再次 resume);另有周期 reaper 兜底扫描。
机制与安全门详见 lib/idle-release.js。注入服务:sessions / agents / sessionPersistence /
webServer / tools / settings(agentPresets / sessionQuery 可选)。
能力
1. Agent 工具 session_send_message
任意会话内的 AI 直接调用,向目标会话投递消息:
- 目标可用「会话地址」(session id)或「会话命名」(对话标题 / 项目 / 工作区名);
- 按命名指定时先解析到唯一地址再投递,命中多个时返回候选清单供你改用地址;
- 排队消息合并:目标 inbox 已有本插件排队未消费消息时,格式化原位追加而非堆积多轮;
- 目标离线(parked)时「投递即唤醒」:
agents.resume加载后 steer(W255 单飞 / W270/W282 setup 兼容)。
2. HTTP API(挂在 dsh web 进程的 webServer 上,127.0.0.1 监听)
| 方法 | 路径 | 用途 |
|---|---|---|
| GET | /celes-center/bridge/health | 健康检查 |
| GET | /celes-center/bridge/sessions | 列出全部会话(地址/标题/项目/在线/running) |
| GET | /celes-center/bridge/memory-state | W287 空闲 agent 释放健康面(免认证只读):agents 总数/running/tracked/累计释放/失败计数/最近释放 |
| POST | /celes-center/bridge/send | 投递消息 {target, content, from?} |
| POST | /celes-center/bridge/set-model | 切换会话模型 {id, provider?, model?, reasoningEffort?} |
| POST | /celes-center/bridge/archive /unarchive /set-title /group-consolidate | 会话运维(归档/反归档/改名/分组整合) |
3. W287 空闲 agent 释放
- 配置:
IDLE_RELEASE_TTL_MS(默认 900000ms=15min,0=关闭)、IDLE_RELEASE_SWEEP_MS(默认 300000ms=5min,0=关闭周期扫描)、auditPort(默认 3180,释放审计回写端口); 可被环境变量IDLE_RELEASE_TTL_MS/IDLE_RELEASE_SWEEP_MS覆盖(settings 表单同源默认)。 - 投递后释放:deliver 的 resume-steer 路径在 agent/status 回 idle 后启动 TTL 计时;期间新投递 (/send 命中 live)/新 turn(running 事件)取消计时;到期且仍非 running → 释放。
- 通用兜底 reaper:周期扫描登记表,覆盖漏发 agent/status 事件的常驻 agent(watchdog/follow 加载)。
- 安全边界:绝不释放 running;绝不释放 TTL 内有活动/排队工作的;失败静默+计数(memory-state 可查)。
- 审计:每次释放 POST
/celes-center/audit/append(server-ops 类目,sessionId+驻留时长+原因)。
配置
| 字段 | 默认 | 说明 |
|---|---|---|
| routePrefix | /celes-center/bridge | HTTP 路由前缀 |
| token | "" | 共享访问令牌(空 = 仅信任本机回环) |
| projcachePath | /opt/dsh/storages/session_projcache.json | 会话标题投影缓存 |
| workspacesPath | /opt/dsh/storages/workspace.json | 工作区注册文件 |
| groupRoots | [] | 分组整合根目录(空 = 关闭) |
| mergeQueued | true | 排队消息原位合并优化 |
| IDLE_RELEASE_TTL_MS | 900000 | 空闲 agent 释放 TTL(毫秒;0=关闭) |
| IDLE_RELEASE_SWEEP_MS | 300000 | 兜底 reaper 周期(毫秒;0=关闭) |
| auditPort | 3180 | 释放审计回写端口 |
兼容性(DSH 0.1.2/0.1.3 破坏性变更适配)
0.1.2 起 Session.events 形态收紧:snapshotEvents(fromSeq?, toSeqExclusive?) / eventAt(seq) / seq
替代;0.1.3 起 persistence.list() 返回 [{header, revision, sizeBytes}]、inspect()/服务级 append()
移除(改 open(id,"read"|"write") 句柄)。本插件的双向兼容策略(见 lib/events-adapt.js):
live Session:优先
session.snapshotEvents()(0.1.2-rc.1+),无此方法时回退session.events(0.1.1-rc.2 及更早),升级窗口期两个核心版本均可运行。persistence.inspect() / loadStored() 返回的检查对象(
SessionInspection/StoredPrefix): 均为纯数据对象{ meta, inheritedEventCount, events }—— 不是 Session,没有 snapshotEvents(),events字段在两个版本都保留,故沿用inspection.events;适配层保留snapshotEvents兜底分支防御收紧。0.1.3 resume 不再携带 preset 挂载:投递唤醒路径补 setup(preset 投影/持久化 header 回退), 工具面不坍缩(W270 §5.3-1 / W282)。
W287 空闲释放基于 0.1.3 语义:
AgentHandle.dispose()停止 loop、await 退出、卸载 scoped world、 关闭会话写句柄、从 agents/sessions 注册表摘除;磁盘日志保留 →agents.resume可再次加载。 0.1.1 宿主无该语义(无 dispose 句柄)时本功能自动降级为只跟踪不释放(track 空 handle,日志提示)。W289 标题兜底缓存失效制改造:无标题离线会话的标题来自日志整读(大文件可 ~秒级), 旧「固定 60s TTL」每 ~60s 强制全量重读 → ~62s 周期 ~10s 同步冷重建窗口(W286 P0 归因)。 现改为按持久化索引失效 + stale-while-revalidate:
persistence.list()行{header, revision, sizeBytes}(0.1.3 契约;revision=dev:ino:size:mtimeNs:ctimeNs) 与缓存 token 对比——文件未变 → 缓存永续;文件变化 → 先回旧值 + 后台异步刷新 (singleflight 去重),请求路径不阻塞;仅首次无缓存值同步取(冷启动一次性)。 实现见lib/title-cache.js(TitleCache.frameEval按目录帧原子求值)。
W780(0.2.7)修复:resume 取错事件字段——已归档 worker 会话「投递成功但什么都没发生」
- 症状:向已归档(parked)worker 会话发消息,
session_send_message/POST /send返回{ok:true, delivery:{via:"resume-steer", live:true}},但目标会话毫无动静——日志里每轮唤醒都是turn/start → step/start → step/end → turn/end,reason.kind=error,message = agent "…" has no provider/model: set AgentOptions.provider and AgentOptions.model, 1 个空步、0 token、0 工具调用。消息确实投进去了、turn 也确实开了,只是 agent 没有模型配置,第一步就抛错。 - 根因:
inspectStored()(lib/index.js)把handle.read()的返回值当数组用。 DSH 0.1.5 的 jsonl 后端(dsh-session-persistence-jsonl/lib/index.js的read()/readPrimed()/readCurrent())返回的是事件切片包装对象{eventState, events}, 不是数组;且persistence.inspect在该后端上不存在,所以每次都走这段 fallback。 于是inspectionEvents()的Array.isArray(inspection?.events)判假 →undefined→ resume 分支的agentOptions提取整块被跳过 →agentOptions = {}→agents.resume({resumeSessionId, agentOptions:{}})恢复出的 agent 无 provider/model。 - 影响面:不止 resume 的模型配置,同一读链上的三处一并失效——离线标题兜底
(
titleFallback,无标题离线会话标题丢失)、离线投递POST /send(误报「无存储日志,无法离线投递」)、 离线POST /set-title(同样 404)。test/inspect-stored-read-shape.test.js用例 e 固定了该回归。 - 修复:新增
eventsOfReadResult()解包——数组直接返回,{events:[…]}取.events, 其余(undefined/非数组)一律[],绝不外泄包装对象;meta仍取handle.header,finally { handle.close() }与read()抛错向上抛的语义均保持不变。双形态兼容(0.1.5 包装对象 + 旧版裸数组)。 - 证据:真实被唤醒会话日志
session-b5bbe03e-…的request/header事件确实带{"config":{"provider":"celestea","model":"deepseek-flash","reasoningEffort":"high",…}}——修复后这条提取路径能取到正确值。 - 自证伪:把
eventsOfReadResult()调用回退成裸handle.read(),test/inspect-stored-read-shape.test.js7 例中 3 例必红(a/d/e;agentOptions变为{provider:undefined,model:undefined})。
单测:npm test(node:test,零依赖)——test/events-adapt.test.js(事件适配)、
test/apply-flow.test.js(投递/目录/模型链路)、test/idle-release.test.js
(W287:TTL 计时/取消、idle 才释放、running 绝不释放、reaper 扫描、审计/计数、apply 集成含 memory-state)、
test/title-cache.test.js(W289:文件未变缓存永续 / 文件变化异步刷新且先回旧值 /
并发刷新去重 / 冷启动同步;apply 集成含响应形状回归)、
test/inspect-stored-read-shape.test.js(W780:handle.read() 包装对象/裸数组双形态解包、
inspect 优先契约、read() 抛错向上抛、resume agentOptions 端到端提取、离线投递不再误报无日志)。
MIT
