opencode-session-spawn
v2.6.0
Published
opencode 会话生成原语(V1+V2 双模兼容):创建新顶层会话并注入起始语句、向指定会话注入消息、只读列举派生会话,用于无人值守长任务编排(V1:工具 spawn_session / notify_parent / notify_session / list_sessions + 命令 @spawn;V2:工具 spawn_session / notify_parent / notify_session + 命令 /spawn)。
Maintainers
Readme
opencode-session-spawn
opencode 插件(V1 + V2 双模兼容,同一 npm 包):会话生成原语。做四件事——创建一个新的顶层会话,并把起始语句注入为该会话首条消息(新会话保持顶层,会出现在会话列表中,便于人工查看;V1 下默认还会让 UI 选中它);子会话反向通知父会话(notify_parent:注入一条 [relay] 消息并唤醒父会话);向任意指定会话注入并唤醒它(notify_session:父/任意会话向已知会话投递指令/纠偏/叫停);以及只读列举派生会话(list_sessions:按父链接发现直接 / 传递派生的会话 id,仅 V1)。
它的用途是无人值守长任务编排:当一条任务超出单会话上下文时,当前会话的模型可以在需要时自行生成一段交接语句、调用工具开出一个新会话继续,新会话拿到语句立即开跑;子会话完成 / 受阻时可用 notify_parent 回调 spawn 它的会话;父会话也可用 list_sessions(V1)找回(可能由中间会话间接派生的)会话 id,再用 notify_session 向指定会话投递纠偏 / 叫停指令。插件不落文书、不读文书、不追踪链、无状态文件——需要交接文书时,把「读取某份文书路径」等引导写进起始语句即可,与其他工具天然兼容。
双模(V1 / V2)兼容
同一份代码、同一个包,default 导出 { id, setup, server }:
| 宿主 | 加载方式 | 行为 |
|---|---|---|
| V1(opencode ≥1.18.29 对象入口) | 读 default.server,调 server(input, options) 取 V1 Hooks | 现 2.5.0 全部功能:4 工具(spawn_session / notify_parent / notify_session / list_sessions)+ @spawn 文本命令(含 @relay spawn) |
| V2(opencode 2.x) | 读 default = { id, setup },调 setup(ctx) | 3 工具(spawn_session / notify_parent / notify_session)+ 注册型命令 /spawn;平台缺口按下表显式降级 |
opencode 1.19.x 过渡版会同时调用
server()与setup(ctx)(后者传入的上下文尚不完整,无tool/session域)。插件已做能力探测:上下文不支持 V2 注册 API 时setup直接返回,由server()承担,不会报错。
V2 能力矩阵与降级(显式记录)
| 能力 | V1 | V2 |
|---|---|---|
| 建会话 + 注入起始语句 | client.session.create({ body, query }) → promptAsync(回退 prompt) | ctx.session.create({ title, metadata, model, location }) → ctx.session.prompt({ sessionID, text }) |
| agent 参数 | 注入时 body.agent(单条消息模式) | 注入前 ctx.session.switchAgent(会话级切换,会持久改变目标会话 agent;V2 prompt 无 per-message agent 字段) |
| 父链接 metadata.spawnParentID | ✅ | ✅ |
| 创建后自动选中新会话 | tui.selectSession(回退 tui.publish) | ❌ 无对应 API(no-op,不自动选中/打开;select 参数保留仅为对齐) |
| tui.showToast 反馈 | ✅ | ❌ 无 tui 命名空间;工具以返回文本反馈;/spawn 命令以 ctx.session.synthetic(resume:false,不唤醒)写入一条 [session-spawn] … 注记 |
| list_sessions 工具 | ✅(client.session.list) | ❌ V2 插件域无 session.list;不注册该工具,请用内置会话列表/搜索 |
| @spawn 文本命令 | ✅ chat.message 拦截(兼容 @relay spawn) | ❌ 不兼容;改用注册型命令 /spawn(见下「为什么 V2 没有 @spawn 文本兼容」) |
为什么 V2 没有 @spawn 文本兼容:V2 的 chat.message 对应物是 ctx.session.hook("prompt", …),它没有显式取消 API。实测(opencode 2.0.30):把 event.prompt.text 置空仍会触发一次模型请求;抛错能让 prompt 请求返回 500(消息不入队但用户看到报错)。两者都不「干净」,且 V1 的 chat.message 本来就只是旁路观察(消息仍会进当前会话),故 V2 只保留注册型命令 /spawn,不做文本兼容。
入口与工具
| 入口 / 工具 | 用法 | 谁触发 | V1 | V2 |
|------|------|--------|----|----|
| 工具 spawn_session | spawn_session({ prompt, title?, agent?, directory?, model?, select? }) | 模型自主调用 | ✅ | ✅ |
| 工具 notify_parent | notify_parent({ text }) | 子会话模型自主调用 | ✅ | ✅ |
| 工具 notify_session | notify_session({ sessionID, text, prefix?, agent? }) | 父 / 任意会话模型自主调用 | ✅ | ✅ |
| 工具 list_sessions | list_sessions({ parentSessionID?, recursive?, directory? }) | 模型自主调用(只读发现派生会话 id) | ✅ | ❌(平台缺口,不注册) |
| 文本命令 | @spawn [--agent <名称>] [--title <标题>] [--no-select] <起始语句>(兼容 @relay spawn) | 人工触发 | ✅ | ❌ |
| 注册命令 | /spawn [--agent <名称>] [--title <标题>] [--no-select] <起始语句> | 人工触发 | — | ✅ |
参数 / 行为:
prompt(必填):注入新会话的起始语句。可内含读取文件、复述要点、执行下一步等引导。title(可选):新会话标题。缺省取prompt首行(清洗非法字符、截断 60)。agent(可选,工具参数):新会话以某 agent/模式启动(如leader)。仅在传了非空白值时透传,交服务端校验。V1 为注入时单条消息模式;V2 为注入前调用ctx.session.switchAgent(会话级)。directory(可选,工具参数):新会话绑定的项目目录(如D:\dev\bbcare)。仅在传了非空白值时透传,交服务端校验。model(可选,工具参数):新会话使用的模型,格式provider/model(如deepseek/deepseek-v4-flash、opencode/big-pickle),透传为{ providerID, id };空 / 无/视为未传,缺省由服务端默认。select(可选,工具参数,默认true):仅 V1。创建后让 UI 选中新会话——优先client.tui.selectSession({ sessionID }),缺失则回退client.tui.publish({ type: "tui.session.select", properties: { sessionID } });失败 / 不可用仅记录,不影响创建与注入。TUI 即时生效;桌面端 app 尚未消费该事件(上游 issue #45963),当前为 no-op,接线后自动打开/切换到该会话 tab。命令--no-select可关闭。V2 无对应 API,参数保留仅为对齐(恒不自动选中)。- 父链接(自动,非参数):
spawn_session把调用者会话 id 写入新会话metadata.spawnParentID(context.sessionID非空白时)。不写parentID——桌面端会话列表只拉取顶层会话,写parentID会让新会话变成「子会话」而从列表消失(v2.2.0 的可见性回归);metadata 不影响列表。命令入口(@spawn//spawn)无父链接(人工语义,与 v2.5.0 一致)。 - 命令 flag 说明:
--agent/--title值均为单个 token(不处理引号/空格);--no-select为布尔开关;仅解析命令后前导 flag,起始语句内部再出现的--xxx原样保留;flag 缺值或起始语句为空 → V1 warning toast / V2 synthetic 注记,且不建会话。--title优先于起始语句首行。 - 内部调用(spawn):
createSession→(V1 可选select)→inject;agent/directory/model/metadata未传时不出现对应键。V1 优先promptAsync(异步 fire-and-forget,不阻塞父会话),缺失则回退同步prompt;V2 用ctx.session.prompt(服务端 inbox 承接,同样不阻塞)。 notify_parent行为:仅能通知父会话(不提供任意 session id 参数);读取自身metadata.spawnParentID(旧会话回退parentID)后,向父会话注入[relay] <text>并唤醒它;父会话以服务端默认模式(build)被唤醒;空文本 / 无父会话 / 读取或注入失败 → 返回错误文本(不抛);无重试 / 队列 / 追踪(保持无状态原语)。notify_session行为:向指定会话注入一条消息并唤醒它,与notify_parent相对(父 / 任意会话 → 指定会话)。先校验目标存在,再向其注入文本并唤醒;prefix: false关闭[relay]前缀(默认加),可选agent指定唤醒模式(V1 单条消息模式 / V2 会话级切换)。空sessionID/ 空text/ 目标=自身 / 目标不存在 / 读取或注入失败 → 返回错误文本(不抛),一律不注入。默认允许任意 sessionID(用户明确要求「任意」):可向非派生会话注入,误用可能致互喷 / 环路——请仅对自己已知 / 派生的会话使用,收到通知后按需行动、勿自动回发。list_sessions行为(仅 V1):只读列举会话。数据来自 SDKclient.session.list({ query: { directory? } })(不查 opencode.db、不落状态文件);按metadata.spawnParentID过滤(不用session.children——它走真parentID,而本插件故意不写 parentID)。parentSessionID缺省 = 当前会话;recursive: false(默认)只列直接子会话,true沿spawnParentIDBFS 列全部传递后代(visited 去重防环)。返回紧凑表格文本(每行id | title | directory | parent | updated),无匹配 →无匹配会话。旧客户端list不带metadata时,逐个session.get回退读取(N+1,会话量小可接受)。V2 不注册该工具(V2 插件域无session.list),请用内置会话列表/搜索。- create 失败 → 不建会话;create 成功但注入失败 → 保留已建会话并提示 id,不产生重复会话。
安装(部署到桌面版)
方式 A:npm 包注册(推荐,自动更新)
V1 配置(plugin 单数数组):
{
"plugin": ["opencode-session-spawn"]
}V2 配置(plugins 复数数组;字符串或 { "package": "..." } 对象):
{
"plugins": ["opencode-session-spawn"]
}opencode 启动时自动把最新发布安装到包缓存(~/.cache/opencode/packages/opencode-session-spawn/),重启桌面版生效。发布新版本后重启即自动更新;若缓存未刷新,删除 ~/.cache/opencode/packages/opencode-session-spawn@latest 后重启强制重装。
方式 B:本地目录注册(开发用)
file:///绝对路径 指向本仓库目录(含 package.json):
// V1
{ "plugin": ["file:///E:/dev/fun-projs/opencode-session-spawn"] }
// V2(本地插件目标必须是目录;opencode 1.19.x/2.x 均支持)
{ "plugins": ["file:///E:/dev/fun-projs/opencode-session-spawn"] }开发与生产验证统一走
test/test.mjs+RELAY_MAIN(见下节)。本地文件形态(单个.mjs)在 V2 会被拒绝:configured plugin path must be a directory。
编排示例
模型在长任务中决定交接,直接调用工具(可选 agent / directory):
spawn_session({
prompt: `你是本任务的接手会话。先读取交接文书:
E:\\proj\\handoff\\migration-3.md
通读后复述要点(已完成/未完成/下一步第 1 件事),确认后继续执行。`,
title: "[迁移] 第 3 棒",
agent: "leader",
directory: "D:\\dev\\bbcare"
})或人工在会话里发(V1;前导 flag 可省略、顺序不限):
@spawn --agent leader --title "[P1-10] sprint" 读取 handoff/migration-3.md 后继续迁移任务V2 用注册型命令(同样支持 --agent / --title / --no-select):
/spawn --agent leader --title "[P1-10] sprint" 读取 handoff/migration-3.md 后继续迁移任务父子回复约定(防环路):父会话在起始语句里要求「子会话收尾 / 受阻时调用 notify_parent」;子会话执行:
notify_parent({ text: "P1-10 已完成,验收报告见 runs/P1-10-acceptance.md" })父会话会收到一条 [relay] … 消息并以服务端默认模式被唤醒;收到通知后按需行动,不要自动回发通知(notify_parent 只向父会话发送,避免环路)。
父会话向指定(可能由中间会话间接派生的)会话投递纠偏 / 叫停:
list_sessions({ recursive: true }) // V1:找回派生会话 id(默认只列直接子会话;recursive 含全部传递后代)
notify_session({ sessionID: "ses_xxx", text: "停止当前分支并汇报进度" })
notify_session({ sessionID: "ses_yyy", text: "仅执行下列修正:…", prefix: false })list_sessions 只读、走 SDK(不查数据库),按 metadata.spawnParentID 发现会话(V1 专属);notify_session 目标须已存在,默认加 [relay] 前缀(prefix: false 关闭),亦可带 agent 指定唤醒模式。
开发与测试
npm install # 安装 devDependency @opencode-ai/plugin(仅 V1 工具注册用;V2 路径懒加载该依赖)
node test/test.mjs # 211 断言(A–M 分组:V1 行为 A–K + 双模契约 L + V2 setup M)test/test.mjs默认经相对路径../index.js测项目版;环境变量RELAY_MAIN覆盖为其他路径(tarball 解包版 / 包缓存活实例)。RELAY_MAIN目标须能向上解析到@opencode-ai/plugin(解包放项目内即可)。- 以尾部
==== 结果: N 通过, 0 失败 ====为准。 - V2 行为由 mock
ctx(tool/command transform 编辑器 + session.create/get/prompt/synthetic/switchAgent 假实现)驱动default.setup()断言。
发布
bump 版本号(package.json 与 index.js 头注释同步)→ node test/test.mjs 全绿 → npm pack 解包核对哈希 + RELAY_MAIN 回归全绿 → commit → push → npm publish(不打 git tag;先 npm whoami 确认登录态,凭据不入库)→ 重启桌面版实测 @spawn(V1)/ spawn_session / /spawn(V2);实测不过修复补丁重发。详见 AGENTS.md。
历史
前身 opencode-session-relay 的接力链、两阶段交接文书、空闲事件驱动、哨兵兜底、@relay status/leave/refresh/verify 等机制已全部移除,仅保留「建会话 + 注入语句」这一核心原语。
- v2.6.0(2026-10-17):V1+V2 双模兼容(同一分支、同一 npm 包)。default 导出改为
{ id, setup, server }:V2 宿主走setup(ctx)(注册 3 工具 + 注册型命令/spawn;@opencode-ai/plugin懒加载解耦);V1 宿主走server(input, options)(行为与 2.5.0 完全一致:4 工具 +@spawn)。V2 平台缺口显式降级:无自动选中、无 toast(命令以synthetic(resume:false)注记)、无list_sessions、不做@spawn文本兼容(实测 prompt hook 无干净取消 API)。opencode 1.19.x 过渡版同时调server/setup,已做能力探测防冲突。 - v2.5.0(2026-10-14):新增工具
notify_session(向任意指定sessionID注入[relay]消息并唤醒;prefix/agent可选;先校验目标存在;默认允许任意会话)与list_sessions(只读、走 SDKclient.session.list、按metadata.spawnParentID发现直接 / 传递派生的会话,不查数据库)。用于「父 / 任意会话 → 指定会话」的纠偏 / 叫停通道与会话发现(填补notify_parent只能上行、task操作不了spawn_session顶层会话的缺口)。 - v2.4.0(2026-09-15):创建后默认让 UI 选中新会话(
client.tui.selectSession,回退tui.publish事件;select: false/@spawn --no-select关闭)。TUI 即时生效;桌面端 app 尚未消费tui.session.select(上游 issue #45963),当前为 no-op,接线后自动开 tab。 - v2.3.0(2026-09-15):父链接由
parentID改为metadata.spawnParentID(修复 v2.2.0 可见性回归:写 parentID 会让新会话变成子会话、从桌面端会话列表消失;且子会话默认不能再开task子代理,会破坏编排);notify_parent兼容旧会话的parentID回退;新增可选model参数(provider/model)。 - v2.2.0:新增
notify_parent反向通知;spawn_session自动挂parentID。 - v2.1.0:
spawn_session支持agent/directory;命令@spawn支持--agent/--title。
