ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Munder Difflin 消息队列剖析:一条指令如何安全地键入 Agent 终端

Munder Difflin 消息队列剖析:一条指令如何安全地键入 Agent 终端 Munder Difflin 消息队列剖析一条指令如何安全地键入 Agent 终端【免费下载链接】munder-difflinA local multi-agent harness that works with your existing Claude Code, Codex subscriptions, allows you to run an office of agents项目地址: https://gitcode.com/GitHub_Trending/mu/munder-difflinMunder Difflin 的每个 Agent 都在真实 PTY 中运行真实的 CLIClaude Code、Codex 等而这条输入行同时被用户和 Harness 争夺。本文以仓库文档 docs/message-queue.md 为骨架完整讲解 Munder Difflin 的MD 消息队列契约谁有权键入、何时键入、为什么这样设计并深入 useHive.ts 的 drain loop、terminalAutomation.ts 的纯策略层与 terminalPool.ts 的终端池实现还原从入队、门控到最终写入 PTY 的完整调用链。1. 一锤定音只有一个门能自动往 PTY 里打字1.1 两个同名却完全不同的队列文档开篇就划清了概念边界——项目里有两个都被称作 the queue 的东西但它们是两回事名称存在于存放内容MD queueHarnesszustand store按 Agent 隔离由Munder Difflin暂存、等待该 Agent 终端空闲的消息Claude queueClaude Code 进程内部Claude Code 已接收但尚未开始处理的文本本文全部内容围绕MD queue。Harness 既看不到 Claude queue也从不参与它的调度——那是 CLI 自己内部的消费队列。1.2 唯一写入者drain loopMunder Difflin 中自动把消息键入运行中 Agent 的 PTY 的唯一位置是 drain loop即 useHive.ts 的 effect #4。所有想触达运行中 Agent 的写入方——composer、Slack 入口、收件箱 nudge、定时 /compact——都统一enqueueMessage(agentId, text)入队然后把何时交付的判断权全部交给 draincomposer / Slack ingress ─┐ inbox nudge (effect #3) ──┼──▶ enqueueMessage(agentId, text) ──▶ MD queue ──▶ drain (#4) ──▶ PTY scheduled /compact (#6) ──┘这是承重设计load-bearing。文档记录了一个真实事故当收件箱 nudge 直接写终端时它成了第二个写入者对提示符是否空闲有自己的一套判断——结果它的文本落在用户写到一半的行上nudge 和用户句子被拼成一条乱码提示一起提交。改为统一入队后一个循环独占所有终端是否空闲的决策nudge 自身不再需要任何提示符逻辑。唯一例外是 god Agent 的启动序列useHive.ts它会直接写入/remote-control命令和 orientation prompt因为那个 PTY 是几百毫秒前刚 spawn 的处于 boot-grace 窗口保护之下不存在用户草稿会被覆盖的问题。源码中BOOT_GRACE_MS 35_000useHive.ts期间所有自动写入方都会被挡在门外等 TUI 先画完它的 banner。1.3 触发节奏防抖 兜底drain 的调度方式useHive.ts每次 store 变化都会触发但防抖 200 mssetTimeout(flush, 200)让 PTY 输出突发合并在一次 flush 里同时存在一个3 秒兜底定时器setInterval(flush, 3000)保证即使没有任何 store 变化队列也不会被饿死每次 dispatch 之间还有4500 ms 的发送冷却FLUSH_COOLDOWN_MS避免背靠背发送把 TUI 卡死。2. drain 到底检查什么六道关卡队列头部的消息只有在全部条件成立时才会被交付。综合 docs/message-queue.md 与 drain 实现useHive.ts完整关卡如下条件原因Agent 状态为idle不打断进行中的 turn自动交付未暂停——或该消息被手动放行楼层级开关Command Center见下文已过 boot-grace 窗口CLI 还在绘制 bannerisTerminalAutomationSafe(ptyId)为真用户拥有提示符见第 3 节距该 Agent 上次交付 ≥ 4.5 s背靠背发送会卡死 TUI交付前重新检查消息的precondition队列项在入队时判定、交付时可能已失效2.1 手动放行v0.3.5 引入当楼层级自动交付被暂停时每条排队消息都会显示一个send now链接。点击后store 中的 releaseQueuedMessage将该消息标记为manual: true移到队首drain 只为它绕过暂停检查——表中其余所有条件依然生效。也就是说手动放行的消息仍然要等待 idle、仍然尊重你的草稿和 picker、仍然以同样的方式确认交付。暂停闸门则拦住其他所有消息。源码注释明确写道Idle/draft/picker safety below still applies to manual messages; only the pause is bypasseduseHive.ts。2.2 只有两次 PTY 写入都成功才确认交付交付确认发生在两次 PTY 写入都成功之后先写文本多行文本会包上 bracketed-paste 标记ESC[200~ … ESC[201~防止嵌入的换行被当作回车逐行提交等待 140 ms再写入提交用的\ruseHive.ts。writePty对死 PTY从不 reject而是返回{ ok: false }——源码注释记载了因此修复过的真实 bug#36未检查返回值会让失败的交付看起来成功drain 于是销毁了实际上从未送达的消息。现在失败的写入会抛错消息保留在队列中可见并自动重试最多重试MAX_SEND_ATTEMPTS 3次超过后带着console.warn被丢弃——有界避免 drain 在一具尸体上永远空转有声保证损失可诊断useHive.ts。2.3 队列层的幂等防抖store 的 enqueueMessage 还内置两条去重规则每 Agent 至多一条待交付/compact——compact 是最坏意义上的幂等第一条生效后其余都只会得到nothing to compact白白消耗一次交付槽和一次模型往返每 Agent 至多一条收件箱 nudge——第一条 nudge 就会让 Agent 清空整个收件箱后面的 nudge 只会落在一个已经清空的目录上。压抑副本不丢任何信息存活的那条 nudge 会把 Agent 带到同一个权威目录。这两条不变量收敛在 store 层而非各调用点是因为调用点太多上下文触发、god 派单、Slack、composer任何一处自行检查都可能被下一条新路径绕过。3. 用户拥有提示符isTerminalAutomationSafe 的四道闸isTerminalAutomationSafeterminalPool.ts是保护你打字的安全闸。它把终端状态投影成TerminalAutomationState交给纯策略函数 terminalAutomationBlock 判定。以下任一为真就拒绝交付阻塞类型由什么触发由什么解除exitedPTY 进程退出重新 spawnpicker你提交了裸的/model类命令打开菜单在该终端键入 Enter / Escape / Ctrl-C或过期draft提示符上有未提交文本提交或清空或过期settling行被释放后短暂的 TUI 重绘窗口时间500 ms3.1 picker 的判定只有裸命令才算opensInteractiveTerminalUiterminalAutomation.ts对裸命令才返回 true/model会打开选择器而/model sonnet带参数直接应用并回到提示符没有需要关闭的 UI。源码注释记录了一个教训只匹配第一个 token 会把第二种形式也锁住而锁一旦锁上没有任何路径能清除——该 Agent 的消息队列在后半场会话中悄悄停止投递。匹配实现只认不含空白的完整裸命令const trimmed input.trim().toLowerCase(); if (/\s/.test(trimmed)) return false; return INTERACTIVE_COMMANDS.has(trimmed);完整的交互式命令清单INTERACTIVE_COMMANDS包括/model、/reasoning、/permissions、/permission、/provider、/settings、/config、/experimental、/experiments、/hooks、/mcp、/apps、/plugins、/resume、/sessionsterminalAutomation.ts。测试 test/terminal-automation.test.cjs 明确验证了这一对行为/model、/provider判定为 true而/model sonnet、/permissions allow、implement this判定为 false。3.2 30 分钟过期窗口宁可多等不可误判picker和draft都会在30 分钟后过期STALE_PICKER_MS与STALE_INPUT_MS均为1_800_000msterminalAutomation.ts。过期机制存在是因为两个标志都是推断而非上报的一个以无法观察的方式关闭的 picker或一个被 TUI 吞掉按键而残留的 draft 标志都会让该 Agent 的 MD 队列在此后整个会话中被卡死。过期行为有两条铁律自动化永不擦除你的文本。过期意味着排队消息被键入到行上已有内容之后两者融合成一条提示。早期版本会先发 Ctrl-U结果静默销毁了那些只是放了一分钟的真实草稿。自动化永不关闭你的菜单。面对 picker 我们不发 Escape。你可能是有意打开后走开的为了让位给排队消息而关闭它不是 Harness 该做的决定——而且我们根本无法验证 Escape 是否真的关掉了菜单。composer 里有一个专门的按钮做这件事因为那样是你请求的。源码中clearTerminalDraft的注释补充了另一层谨慎terminalPool.tsCtrl-U 清行时不清除automationBlocked闩锁——Ctrl-U 只杀输入行不会关闭已打开的 picker。早期版本清了它导致排队消息被键入到 picker 里还被确认为已交付——消息丢失picker 收到垃圾。两个窗口都刻意设得很长。把活跃草稿当作废弃是昂贵的错误让排队消息多停一会儿是便宜的错误。测试断言十分钟后依然归用户所有半小时后才放行test/terminal-automation.test.cjs。4. 读屏幕而非建模promptLineHasText 的单向校正4.1 幽灵草稿问题inputDirty是通过在term.onData中计数按键推断的terminalPool.ts。这个模型会漂移一个把按键吞进自己 UI 的 TUI会在提示符肉眼可见为空时让计数停在非零——这就是幽灵草稿phantom draft它会把 MD 队列挡住而实际上并没有草稿存在。4.2 直接读渲染缓冲xterm 已经持有渲染好的屏幕所以 promptLineHasText 直接读取它取term.buffer.active中baseY cursorY那一行用PROMPT_CHROME正则/[─-╿\s❯$#|]/g剥掉 TUI 画在输入行周围的装饰框和提示符标记剩下的非空即认为有文本。它是刻意单向的缓冲读取只能清除草稿绝不能发明草稿。屏幕显示为空 ⇒ 相信它解除阻塞屏幕显示有文本或读不到 ⇒ 回退到按键计数。这个不对称是设计的核心因为两种错误的代价不同错误的空会打开闸门把排队消息融合进你正在写的东西错误的有文本只是把消息停到草稿过期。所以允许发生的恰恰是便宜的那种错误。4.3 ECHO_GRACE_MS回显间隙不是证据有一个场景屏幕完全不能当证据inputDirty在你按下按键的瞬间就被置位但字符要等 PTY 回显后才到达 xterm 的缓冲。在这个间隙里缓冲仍显示旧状态——对刚开头的草稿读取会返回空也就是昂贵的那个方向。因此距最后一次按键 **1 秒内ECHO_GRACE_MS 1000**的读取返回不知道null不清除任何东西terminalPool.ts。4.4 配套的解析细节term.onData中还维护了一个与inputDirty同步的lineBuf当前行模型裸 Escape / Ctrl-C释放 picker 闩锁并清空 lineBuf箭头键的转义序列不会清除否则用户在 picker 里导航时阻塞会被误清用户自己的 Ctrl-Ukill-line清空 lineBuf与我们发送的行为完全一致bracketed paste只剥掉包裹标记粘贴内容仍视为用户草稿——ESC[200~开头意味着这是一次粘贴而非提交其内嵌换行不会被当成多次提交。每次按键都会重新盖时间戳inputDirtyAt所以过期时钟度量的是用户最后一次触碰草稿至今而非开始至今。5. 看见为什么没在投递typing 徽章一个被卡住的 MD 队列过去看起来和一个无事可做的 idle Agent 一模一样。typing徽章CSS 类--cth-status-typing文案your draft解决了这个问题只要hasTerminalDraft(ptyId)为真它就会渲染在 Agent 卡片和全屏 roster 上terminalPool.ts。三个关键设计它不是 Agent 状态也从不存到 Agent 上——那个字段归 PTY 解析器所有存上去会被覆盖。它是在渲染时从 gate 使用的同一套草稿检测派生出来的所以徽章报告的就是 gate 看到的同一个草稿。渲染端用 useHasTerminalDraft 以 1 秒间隔轮询标志活在可变池条目上没有组件订阅它每次只读一行缓冲开销可忽略且徽章一秒的延迟不可见。它不应用gate 的 30 分钟过期。窗口过后 gate 开始投递而徽章仍显示 your draft——这反而诚实你的文本确实还在提示符上。徽章回答的是我的文本在不在而不是队列有没有被阻塞。6. 完整调用链一条消息从入队到落进终端综合前面所有环节一条消息的生命周期如下入队任意来源composer、Slack ingress useHive.ts、收件箱 nudge effect #3 useHive.ts、voice bridge、上下文触发 effect #6 useHive.ts调用 store 的enqueueMessage消息以QueuedMessage含 id、text、ts、可选的 slack/instruction/precondition/compactUsed 元数据追加到该 Agent 的队列尾部并持久化。调度store 变化触发防抖 200 ms 的flush()3 秒兜底定时器兜底。flush对每个有队列的 Agent 调用dispatch。门控dispatch依次检查 idle/quiet、autoDeliveryPaused除非manual、boot-grace、isTerminalAutomationSafe、4.5 s 冷却、交付前precondition复查。写入通过 submitToPty 写入。同一 PTY 的写入被串行化writeChainspromise 链防止并发调用者把输入挤在一起多行文本包 bracketed-paste单行文本裸发部分 TUI 如 Antigravity 的 agy 会把粘贴标记当字面输入。确认两次写入文本 \r都成功后才removeQueuedMessage失败保留重试3 次后带 warn 丢弃。Slack 来源的消息在首次派发时还会被提升为 kanban 卡片ensureSlackCard。7. 代码地图每个文件扮演的角色文件角色terminalAutomation.ts纯策略——阻塞判定、过期窗口、交互式命令识别。无 DOM完全单元测试覆盖terminalPool.ts按 PTY 池化的 xterm 实例缓冲读取、闩锁、isTerminalAutomationSafe、hasTerminalDraftuseHive.tseffect #3 收件箱 nudge入队、effect #4 drain唯一写入者、effect #6 定时 /compactstore.tsMD 队列本体 Agent 持久化enqueueMessage/removeQueuedMessage/releaseQueuedMessage/clearQueueterminal-automation.test.cjs纯策略层的单元测试交互式命令识别、四类阻塞、过期语义、阻塞优先级如何验证这套行为策略层是完全脱离 DOM 的纯函数可以在 Node 中直接运行测试node --test test/terminal-automation.test.cjs该测试验证了本文讨论的核心语义/model阻塞而/model sonnet不阻塞draft/picker/exited/settling 四类阻塞30 分钟过期后放行picker 优先级高于 draft两者同时为真时报picker。仓库根目录 package.json 提供了完整的测试脚本入口load-ts.cjstest/load-ts.cjs负责把 TS 源转译后加载进node:test。8. 设计哲学小结读完整个消息队列契约可以提炼出几条贯穿始终的设计原则单一写入者。一个 drain loop 独占终端是否空闲的决策权任何新功能要触达 Agent 终端先入队再说。用户的提示符神圣不可侵犯。自动化永不擦除用户文本、永不关闭用户菜单两个 30 分钟窗口刻意设长因为多等比误删便宜得多。屏幕是更好的证据但要单向使用。缓冲读取只能清除幽灵草稿、不能发明草稿并把回显间隙排除在证据之外。失败的交付必须可见。两次写入都成功才确认失败保留重试有界、有声地丢弃绝不静默吞掉消息。昂贵的错误由人来做。关闭 picker 的按钮放在 composer 里只有用户自己可以按。这套契约是 Munder Difflin 能让用户连续向运行中的 Agent 发消息、却从不在打字时被自动消息干扰的根基——它不依赖任何 CLI 的配合纯粹在 Harness 这一侧用可验证的、带失效机制的推断守住了一条消息只能出现在它该出现的地方这条底线。【免费下载链接】munder-difflinA local multi-agent harness that works with your existing Claude Code, Codex subscriptions, allows you to run an office of agents项目地址: https://gitcode.com/GitHub_Trending/mu/munder-difflin创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表