Replies: 5 comments 1 reply
|
@TT-Wang 你的 #6030 我把代码路径逐行核过(master 5dda764,0.1.5-alpha.1),诊断全对——steer 只入队不抢占、next-step 消费点只有 step 边界一处、job_output 的 wait 只有三出口。另外你的「插件可以先行验证首选方案」判断也成立,而且现有的 in-tree wrapper 就是现成模板。以下补三处源级事实 + 对两个设计问题的回答。 ① 源级确认(与你的行号一致)
② 插件先行验证——seam 已存在,且有官方模板
ctx.on('tools/execute', async (exec, next) => {
// ... 判定是否目标工具
const upstream = exec.signal
exec.signal = d.signal // 用派生 signal 替换
try { return await next() }
finally { exec.signal = upstream } // 还原,post-execute 监听者看不到
})它证明了「wrapper 可以临时替换
这不需要等本体改动,且能实测验证首选方案的语义是否被模型正确消化(模型拿到 "interrupted, still running" 后是去读 job 还是去回应用户)。你提的 ③ 对两个设计问题的回答Q1:wait 因排队输入提前返回 running 是否可接受? 可接受,且语义上最干净——但有一个必须明确的判别:wait 提前返回时,那次「排队输入」必须仍留在 inbox 里由后续 preStep claim,wait 只是把控制权还回去,不能吞掉输入。你首选的协作式抢占天然满足(它只 resolve wait,不碰 inbox);次选(steer 转软中断走 abort 路径)要小心别把 inbox 清了( Q2:input-pending 信号由 loop 经 exec 传给工具,还是 jobs 服务问 agent? 倾向前者(loop 经 exec 传给工具),理由:
不过插件先行版不需要等这个设计定案——插件版直接在 wrapper 里轮询 附带:完成通知走同一条路的意义你说 job 完成通知(owner busy 时 一句话:首选方案语义成立、信号源应在 loop 侧;插件先行版现在就能做,官方 timeout-policy 是现成模板,我可以按上面设计实现一版发布,实测首选语义后再回传数据给 upstream 定案。 |
|
@argszero 谢谢核实。你引的两处我也独立对过 你补的 keepInbox 是对的,而且树内已有先例这条我们报告里漏了。补一个证据:session-controller 自己的取消路径就是这么写的—— agent.cancel({ kind: 'user' }, { keepInbox: true })所以「软中断复用 abort 路径但不清 inbox」不是新发明,API 层的 cancel 本来就保 inbox。第三个方案(激进式)的顾虑因此比我原先写的要小。 你插件里有个边界:steer 和 inject 共用同一条队列
steer(input) { this.send(input, 'next-step', true) }
inject(input) { this.send(input, 'next-step', false) }两者进的是同一条 而 job 完成通知在 owner busy 时走的正是 后果:agent 阻塞在 job A 的 wait 上时,job B 完成就会把通知塞进 好在可以区分。用户经 顺带:被 inject 的完成通知本身也卡在同一个 step 边界后面,所以「job B 完成了但模型要等 job A 的 wait 到期才知道」这个延迟是真实存在的。要不要顺手也让它抢占,是个产品选择——但那种情况下的文案得换一套说法,不能说用户在等。 还需要维护者定的目前串里还没有官方的人表态。真正只有他们能决定的是那个信号归属:input-pending 该不该作为只读谓词进 |
|
@TT-Wang 你这条反馈非常及时而且完全正确——你等于在我发布的
根因: 修复:把触发条件从队列长度改成逐条
所以现在只有真实 user steer 会触发抢占;plugin 通知即使塞在同一个队列里也不会误 abort job A 的 wait(那本质上不是用户输入,不该折损模型正在等的那个 job)。 加了两个回归测试(共 10/10 通过):
关于「job B 完成也该抢占吗」你在信里提的那个产品选择我很认同:被 inject 的完成通知因为卡在同一个 step 边界后面,确实存在「job B 完成了但模型要等 job A 的 wait 到期才知道」的延迟。但那应不应该抢占,我觉得不该由这个插件在 v0.1 决定——因为它需要另一套文案(不能说「用户在等」,而是「job B 已完成」),语义和目标不同。这更适合作为下一步的独立增强,或者干脆在官方定夺 signal 归属后处理。 关于需要维护者定的信号归属你我的判断一致: 再次谢谢你把边界挑到这一层——这是「讨论 → 插件 → 发现真 bug → 回源修复」最完整的一次闭环。如果 v0.1.1 的默认行为(pollMs 200 / graceMs 1000,只拦 |
|
@TT-Wang 补一条更正 —— 我上一条公告的 v0.1.1 其实装不上,请用 v0.1.2。 问题:v0.1.1 声明 v0.1.2 已发布,range 改为一条 comparator 对应一条元组线: 实测(空项目装 你提的功能修复一行没动 —— 触发条件仍是逐条 顺带:同一个坑我复核了自家全部插件, |
Uh oh!
There was an error while loading. Please reload this page.
在 0.1.5-alpha.1 上确认:一次阻塞式
job_output(wait: true)期间,用户的 steer 消息要等这次 wait 返回才会被消费,最长 10 分钟。会话在这段时间里对输入完全没有反应,UI 上只显示「深度求索中...」,看起来像卡死,实际进程健康、后台任务也在正常推进。这不是「跑等待命令就该失联」。
steer()的语义是插队到 next-step,本应尽快被消费;问题在于 next-step 的消费点只有 step 边界一处,而一次 wait 可以让 step 边界十分钟不出现。同一状态下agent.cancel({kind:'user'})能在同一 tick 内解除阻塞,说明等待逻辑本身是尊重AbortSignal的,只是 steer 从来不会去触发它。实测
dsh-v0.1.5-alpha.1(commit5dda764ed3,源码构建),隔离的DSH_HOME,macOS / Node v22.22.3:job_output(wait: true),后台 job 是sleep 120session/prompt以mode: 'steer'发一条带唯一标记的消息user/message进入会话日志steer 延迟 115.7 秒,正好等于剩余的 wait 时间。
代码路径(行号为 0.1.5-alpha.1)
steer只入队,不 abort —packages/core/agent-loop/src/agent.ts:141-143对照
cancel,它会 abort 当前 phase —agent.ts:154而 next-step 的唯一消费点在
preStep()里 —agent.ts:244step 卡在 await 里就永远走不到这一行。阻塞侧本身没有问题:
packages/jobs/tool-jobs/src/index.ts:332-333正确 clamp 了超时并把exec.signal转发下去,packages/jobs/jobs-local/src/index.ts:230的wait也确实只有三个出口(job 终态、timeout、signal abort)。缺的是第四个出口。配置上有个容易混淆的地方(
tool-jobs/src/index.ts:205-206):默认阻塞是 30 秒,不是 10 分钟;10 分钟是maxWaitTimeoutMs上限。但模型可以显式把timeout_ms顶到上限,没有任何机制阻止或抢占。tool-jobs的 system prompt 里已经写了「do not busy-poll or sleep on one」,现场会话仍然在做sleep+job_output(wait:true)的组合轮询。靠提示词约束模型行为在这里不成立,需要机制层面的兜底。顺带一提,job 完成通知走的是同一条路(
tool-jobs/src/index.ts:292-299,owner busy 时inject,即 next-step),所以完成通知的延迟特征和用户输入完全一致。版本核对
这个问题最初是在 0.1.3-alpha.2 上遇到的。升到 0.1.5-alpha.1 后逐条复核,上述判据字节级未变;
git diff dsh-v0.1.3-alpha.2..dsh-v0.1.5-alpha.1里tool-jobs和jobs-local两个包零改动。agent.ts改了 111 行,但都是本版 Inbox 重构的搬家,没有触及 steer / claim / abort 的语义。全仓搜preempt/interrupt/pendingInput也没有新增的抢占路径。想讨论的修法
首选是协作式抢占:让
ctx.jobs.wait除了 job 完成和 timeout 之外,也在「调用方有 pending next-step 输入」时 resolve,返回[status: running]并保持 job 存活。语义不变、不丢工作、job 继续跑,只是把控制权还给 loop。实现上需要给 wait 传一个until/ input-pending 信号。次选是收紧上限加界面提示:把
maxWaitTimeoutMs默认从 10 分钟降到 60–120 秒,同时在界面上显示「会话正阻塞于 job X,你的消息已排队」,至少别让用户误判成卡死。还有一个更激进的选项:仅针对阻塞式 wait,把用户 steer 当作软中断,复用现有 abort 路径但不清空 inbox。这个改动面最大,语义上也最需要斟酌。
有意思的是,0.1.5 把
Inbox改成类型接口之后,hasPending/claim退出了公开面,但公开的Inbox保留了nextStep只读数组(packages/core/agent/src/runtime-types.ts:48-52),Agent.inbox也是公开的,工具执行上下文能拿到exec.agent。所以第三方插件现在已经可以包一层job_output、轮询exec.agent.inbox.nextStep.length来先行验证首选方案的语义,不必等本体改动。想问维护者两件事:一是首选方案的语义(wait 因为有排队输入而提前返回 running)是否可接受;二是如果可接受,这个「input-pending」信号更适合由 loop 通过 exec 传给工具,还是由 jobs 服务自己去问 agent。
临时缓解
杀掉底层子进程(比如
sleep的 PID),job 立即结束,wait 返回,会话回到 step 边界后就能消费排队的消息。All reactions