feat(mesh): unattended dispatch loop, mid-run steering, and the build fix - #11294
feat(mesh): unattended dispatch loop, mid-run steering, and the build fix#11294yiliang114 wants to merge 4 commits into
Conversation
…of search #11225 replaced 'if (item)' with 'if (item?.sourceType !== MESH_HOST...)'. The optional chain yields undefined when the read found nothing, and undefined !== 'mesh', so a session that vanished between the search hit and this read now enters the branch and is added as an undefined summary. It fails tsc, which is why every build on the branch is red, and it would have been a real regression even if it had compiled: the guard it replaced existed to drop exactly that case. Both conditions, in this order.
Nothing ticked the dispatcher: dispatchOnce and startMeshHostSessionOwner were referenced only by their tests, and the first live slice was driven by hand. The loop now runs inside the hidden host session. The launcher and the background-agent registry live in that process, so ticking there makes inspect a local registry read and a start a local call; a daemon-side loop would have paid a process boundary per candidate per tick for nothing. The daemon's whole job is to keep the host resident, which it does for any trusted workspace whose roster is non-empty. The supervisor dispatches only while its session still holds the workspace's host claim, so a stale duplicate the reaper did not reach cannot give one agent two bodies. Ticks coalesce rather than overlap, and a tick requested mid-pass joins that pass and receives its outcome. It polls rather than waiting for notifications because posts written by the daemon's REST route land in the store, not in this process. Two stale assertions are aligned with changes that landed after they were written: thread tool names now live in ToolNames, and the prompt window is committed when the runtime accepts the turn rather than at booking.
…d not read A person posting while their agent is mid-turn is the one thing this system does that Multica cannot, and it was scheduled with the reliability work in step 8. That left the path looking supported while it was not: admission coalesced the post onto the running run, charged a turn for it, and nothing ever delivered or rebooked it. The message was silently swallowed, which is the single failure mode this design says it will not have. The dispatcher now pushes pending triggers into a running body before it looks for idle work, using the structured input path with the run id as the delivery id so the drain event can be matched back rather than guessed at from the text. Acceptance is recorded separately from consumption: the queue took it, the model has not read it, and the correlated drain event is still what commits the watermark. The terminal write is the backstop, because it is the last moment at which 'this run will never read it' becomes true. Anything the run was told to answer and did not consume is rebooked as one fresh run. Human and system triggers only: an agent post that missed its target has its author still on the thread, and the turn gate exists precisely to stop two agents re-triggering each other, so replaying one would spend budget repeating a conversation nobody is waiting on.
…d' into codex/mesh-midrun-delivery
|
Closing: redundant. The foundation already has all three — |
|
Thanks for the PR! Template headings all present ✓. The One note before the gate findings, because it shaped this review: the PR changed underneath the review. It started as Problem: observed, and the tree already documented the gap. Not theoretical hardening. Direction: aligned. It closes the gap the design already claims (§0.1, decision 9, §4's Size: core paths, and now cross-package ( Approach: the mid-run steering half is what I would have written. I sketched a fix from the title and the "Why it's needed" section before opening the diff — optional The scope question is the bundling, and I'd like to ask it plainly rather than block on it: the body still describes the old, narrower PR. It says "Stacked on #11263" and lists three changes, but the diff now absorbs #11263 outright ( Two smaller things: the Risk: two signals, one of them hard. Stage 1e matches Flagging these before going deeper — the build break is the one that has to move first. 中文说明感谢贡献! 模板标题齐全 ✓。模板里的 在门禁结论之前先说明一点,因为它影响了本次审查:这个 PR 在审查过程中发生了变化。 它最初是 问题:已观测到,而且代码库里早就记录了这个缺口。 不是理论性加固。基线分支上 方向:一致。 它补上了设计文档已经声称的能力(§0.1、decision 9、§4 的 规模: 触及核心路径,且现在是跨包改动( 方案:mid-run steering 这一半换成我也会这么写。 我在打开 diff 之前只根据标题和"Why it's needed"草拟了修法——port 上可选的 范围上的问题在于打包方式,我想直接提出来而不是以此阻塞:PR 描述仍然是那个更窄的旧版本。 它写着"Stacked on #11263"并列出三项改动,但 diff 现在已经把 #11263 整个吸收进来( 两个小点: 风险:两个信号,其中一个是硬性的。 Stage 1e 命中 先把这些提出来再往下深入——编译失败是必须最先解决的那一项。 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
Code reviewEvidence carried here: the PR's own CI checks and job logs, read through the API, plus a static read of the tree at the reviewed commit. I did not build, run or execute any PR-derived code — this review is static, per the gate's no-execute rule. Blocker:
|
| File | What changed |
|---|---|
packages/core/src/agents/mesh/run-lifecycle.ts |
Adds the terminal-write rebook; does not compile — ThreadRun used but never imported |
packages/core/src/agents/mesh/thread-actions.ts |
Adds acceptRunDelivery: records acceptance and extends the context window, deliberately not consumption |
packages/core/src/agents/mesh/dispatcher.ts |
New steering pass ahead of the candidate loop, plus the deliver port method and two result kinds |
packages/core/src/agents/mesh/dispatch-port.ts |
Implements deliver over queueExternalInput using the structured form so the delivery id survives; unrelated reformat of one call |
packages/core/src/agents/mesh/supervisor.ts |
The whole dispatch loop from #11263, now folded into this PR rather than stacked under it |
packages/core/src/agents/mesh/supervisor.test.ts |
Tests for that loop — also #11263's |
packages/core/src/agents/index.ts |
Barrel exports for the dispatcher, port and supervisor; three of the eight have a consumer |
packages/cli/src/acp-integration/acpAgent.ts |
Starts and stops one supervisor per mesh host session — a Stage 1e revert-correlated path |
packages/cli/src/serve/server.ts |
Daemon-side host owner: keeps one hidden host resident per rostered, trusted workspace on the keepalive cadence |
packages/cli/src/serve/server/session-list.ts |
Restores the existence check when filtering host sessions out — looks like #11261 |
packages/core/src/agents/mesh/capability.test.ts |
Repairs a boundary assertion by filtering thread tool names out first |
packages/core/src/agents/mesh/dispatch-port.test.ts |
Adds the sidecar storage mock the base branch started needing |
packages/core/src/agents/mesh/dispatcher.test.ts |
Steering and refused-delivery cases |
packages/core/src/agents/mesh/run-lifecycle.test.ts |
Rebook cases: consumed not replayed, agent post not replayed, nothing rebooked onto a done thread |
docs/plans/2026-09-06-multi-agent-board-collaboration.md |
Resequences step 8, moving three items ahead of step 7 |
docs/plans/2026-09-07-mesh-implementation-acceptance.md |
Records that mid-run steering landed early, with the observed test count |
Test evidence — the PR's own CI
ci.yml triggers pull_request only on base branches main and release/**, so with this PR based on codex/multi-agent-mesh-foundation the unit, lint and typecheck lanes never fired. tui-parity is not branch-filtered, so it did run — and both of its jobs failed while building, which is the only automated signal this PR has and it is red. Nothing was polled or waited on; this is the complete picture as of the fetch.
| Check | Conclusion |
|---|---|
| TUI parity snapshots (ink vs opentui) | failure — npm run build --workspace=packages/core exit 1, 6 TS errors in run-lifecycle.ts |
| OpenTUI no-flicker gate | failure — same 6 TS errors, same build step |
test (unit, ci.yml) |
not triggered — base branch outside main / release/** |
lint_and_static (ci.yml) |
not triggered — base branch outside main / release/** |
classify_pr (ci.yml) |
not triggered — base branch outside main / release/** |
| assign | success |
| label | success |
| authorize | success |
| Remind on force-push | success |
| delete-asset-branch | success |
| delay-automatic-review | cancelled (superseded by the force-push) |
| review-pr | cancelled (superseded by the force-push) |
The two failures are the same defect reproduced independently by two jobs, not flake: identical file, identical six errors, identical failing build step, on the current head. Classified as PR-caused from the diff and the compiler output — run-lifecycle.ts is a file this PR modifies and the missing symbol is one this PR introduces — not from anything the log body claims.
Not verified: that packages/cli typechecks (the build never reached it); that the mesh suite passes on a clean install (the author's 144-test count is from vitest, which does not typecheck, and is the author's claim about their own machine rather than evidence this run can stand behind); and every behavioural claim above, since no lane ran the changed code.
Sandboxed verification would settle the behavioural half: @qwen-code /verify — that a message steered into a running turn reaches consumedMessageIds and is therefore not rebooked at finish is the whole difference between at-least-once and answering every interjection twice, and it is observable neither from the diff nor from this PR's tests, since every fake port here returns without emitting a drain event. Note that a /verify run would also have caught the compile error in its base build step. @qwen-code /tmux is the lane for driving the mid-run interjection as a real user against the live host, and matters more than usual here given the acp-integration wiring is new. Neither will substitute for fixing the build first — and until this PR targets main or a release/** branch, ci.yml will keep not running on it at all.
中文说明
代码审查
本评论携带的证据:通过 API 读取的本 PR 自身 CI 检查与作业日志,外加对审查 commit 上代码树的静态阅读。我没有构建、运行或执行任何源自 PR 的代码——依据 gate 的「不执行」规则,本次审查为静态审查。
阻塞项:packages/core 编译不过
(六条编译错误见上方代码块,中文不重复。)
我是对照 a3146b04 核实过的,没有只采信日志。run-lifecycle.ts:36 是 import type { Thread, ThreadEvent, ThreadMessage } from './types.js';——列表里没有 ThreadRun——而新增的 rebookUnconsumedTriggers 用了它两次,分别在 398 行(run: ThreadRun)和 414 行(const rebooked: ThreadRun = {)。四条 TS7006 是连带结果:run 无法解析,run.consumedMessageIds 和 run.triggerMessageIds 就成了 any,于是在 noImplicitAny 下 id 和 message 回调参数失去类型。把 ThreadRun 加进那一条 import,六条错误应该都会消失。
这一点值得多说几句,因为它解释了 PR 自己的测试计划为什么看起来是绿的。npx vitest run src/agents/mesh/ 用 esbuild 转译,只剥离类型而不做检查,所以 144 条测试全过的同时 tsc 可以失败——报告里的「14 files / 144 tests」和这个编译失败两件事同时为真。AGENTS.md 工作流第 3 步要求在单元测试之外一并跑 npm run build && npm run typecheck,正是因为这个原因。这也意味着 packages/cli 那一半的类型正确性是未知,而非干净:构建死在 packages/core,编译器根本没有走到 acpAgent.ts、serve/server.ts 和 session-list.ts。
考虑到标题里写着「the build fix」,这有点讽刺——这里打包的两个测试修复(capability.test.ts、dispatch-port.test.ts)确实修好了基线分支的破损,但 PR 自己新增的代码引入了一个新的编译错误。
机制本身是成立的
我对照基线分支的既有机制端到端追了 steering 链路,没有只采信描述。deliver 传入 deliveryId: run.id;runtime-bridge.ts:39 正是用它做 drain 关联(event.deliveryId === input.context.runId);随后 consumeRunDelivery 把 acceptedMessageIds 整体提升进 consumedMessageIds,而这正是 rebookUnconsumedTriggers 的过滤依据。所以模型确实读到的 steer 会被提交,也不会被重订。dispatcher 里的去重依赖 accepted 集合而非 watermark,这个选择是对的——原因见下面第二条。重订 run 上的 attempts: 0 与既有约定一致(run 创建时为 0,由 claimRun 递增),而 bindRunSession 的 consumedOnStart 路径意味着启动触发消息已被消费,因此重订不会把它重放一遍。
- 区分「至少一次」与「每次插话都回答两遍」的那条不变量没有测试。 所有新增测试用的都是 fake port,其
deliver从不触发 drain 事件,因此 accepted → consumed 的提升路径从未被执行到。投递被拒的测试覆盖了 rebook 路径;「不会重复投递」的断言只统计了单个 dispatcher pass 内driver.deliver的调用次数。所以「被 steer 的消息最终会进入consumedMessageIds、因而在结束时不会被重订」没有测试。描述对此是坦率的(「drain 事件那一半由runtime-bridge.ts既有的关联逻辑覆盖,这里没有新增测试」)——我确认了这个关联确实存在且对得上,但那是静态阅读,不是证据。
以下为非阻塞项,大致按我希望被处理的优先级排列:
acceptRunDelivery会在一个必然会发生的竞态上抛异常。 它严格要求run.status === 'running',而姊妹函数consumeRunDelivery接受'running' || 'finishing'。它被调用时传入的是dispatchOnce开头listThreads()读到的过期run快照,中间还经过了await port.deliver(...)和获取 workspace 锁。如果在这个窗口内 run 进入了finishing或终态——registry 仍可能报告running,于是deliver返回true——它就会抛错。steering pass 位于 candidate 循环之前,而dispatcher.ts没有任何try/catch,所以dispatchOnce整体 reject、本次 tick 中止:runPass把它兜成{kind:'error'}、打出dispatch pass failed,排在后面的所有 queued candidate 都要等下一个 tick。约 2 秒后自愈,消息在结束时仍会被重订,所以这是延迟加反复的日志噪音而非丢失——但把投递未命中记录下来而不是抛错就能消除它,也能让两个函数之间对finishing的处理差异变成有意为之。consumeRunDelivery会让contextThroughSequence回退。 它写回through = context.contextThroughSequence ?? run.contextThroughSequence,而context是启动时的 binding,于是acceptRunDelivery刚扩展的值被更旧的快照覆盖(最简单场景下 2 → 1)。今天无害——run 即将终态,prompt 组装读的是deliveryByAgent.committedThroughSequence而非run.contextThroughSequence,去重也依赖acceptedMessageIds——但这让一个被文档描述为高水位的字段变成非单调的;将来若有改动信任 watermark 而非 accepted 集合,就会静默重复投递。至少值得加一句注释。- 重订的 run 绕过了预算门禁。 它被直接以
queued写入 store,而selectCandidates也不做预算检查,所以即便该 thread tree 的 token 预算已耗尽仍会启动。可以辩护——这份工作此前已被 admit 并计费过一次,且不重复增加autoTurnsUsed是正确的,与 human 发帖路径一致——但dispatch-policy.ts把预算规则称为「一个能用的 mesh 与一场 token 火灾之间的区别」并声明其 fail closed。补一句确认这个超支是有意为之即可。 capability.test.ts在修复断言的同时放松了它。 原来是「ToolNames里没有任何工具被分类为thread」;现在是「ToolNames里除了这些被点名的 thread 工具之外,没有工具被分类为thread」,也就是按名字排除了它本来要抓的东西,只剩第二条断言是真正的检查。如果 thread 工具确实迁进了ToolNames,那么过滤是诚实的修正——但请在 PR 描述里说明,因为一个顺手收窄了边界测试的「build fix」正是值得多看一眼的形状。- 部分新增的 barrel 导出没有消费者。
agents/index.ts新增了八个,acpAgent.ts只导入三个(createMeshDispatchPort、startMeshSupervisor、MeshSupervisor)。dispatchOnce、DispatchRecord、MeshTickOutcome和DEFAULT_MESH_SUPERVISOR_INTERVAL_MS被导出到包边界之外,目前无人读取。 allocateRunSequence()现在每次 finish 都无条件调用,而它每次调用都会落盘 workspace 文件,因此即使没有需要重订的内容,每次 run 结束也都会写一次 workspace 记录。属于轻微写放大;若要惰性分配,需要先把missed集合算出来。
核实而非采信: 描述中称为既有问题的 no-unsafe-finally 确实是既有问题——本 PR 没有改动 runtime-bridge.ts,而 finally 里的 throw 在基线 commit 上就已存在。
(时序图与文件清单见上,中文不重复:人在 turn 执行中发帖 → admission 合并到运行中的 run 并计入一次 turn → dispatcher 的 steering pass 以 run id 为 deliveryId 投递 → 队列接受则记为 accepted 并扩展上下文窗口(非 consumed),body 在 tool 轮次之间 drain,runtime-bridge 按 delivery id 关联并把 accepted 提升为 consumed,结束时不重订;队列拒绝则记为 delivery_race,由终态写入重订,且仅限 human/system、不会重订到已 done 的 thread。)
测试证据 —— PR 自身的 CI
ci.yml 的 pull_request 只在 base 为 main 和 release/** 时触发,因此本 PR 基于 codex/multi-agent-mesh-foundation,单元、lint 与类型检查通道全都没有触发。tui-parity 没有分支过滤,所以它确实跑了——而它的两个作业都在构建阶段失败。这是本 PR 唯一的自动化信号,而且它是红的。没有做任何轮询或等待;以上就是抓取时刻的完整情况。
(CI 表格见上方机器可读区域,中文不重复。)
这两个失败是同一个缺陷被两个作业各自独立复现,不是 flake:相同的文件、相同的六条错误、相同的失败构建步骤,且都在当前 head 上。判定为 PR 引入,依据是 diff 与编译器输出——run-lifecycle.ts 是本 PR 改动的文件,缺失的符号也是本 PR 引入的——而不是日志正文里的任何说法。
未验证: packages/cli 能否通过类型检查(构建没有走到);mesh 测试套件在干净安装下是否通过(作者的 144 条测试来自不做类型检查的 vitest,且那是作者关于自己机器的说法,不是本次运行能够背书的证据);以及上面所有行为性结论,因为没有任何通道运行过被改动的代码。
沙箱验证可以了结行为性那一半:@qwen-code /verify——一条被 steer 进运行中 turn 的消息是否会进入 consumedMessageIds、从而在结束时不被重订,正是「至少一次」与「每次插话都回答两遍」之间的全部区别;它既无法从 diff 观察到,也无法从本 PR 的测试观察到,因为这里每个 fake port 都是直接返回、不发 drain 事件。需要说明的是,一次 /verify 运行本身也会在它的基线构建步骤里抓到这个编译错误。@qwen-code /tmux 是以真实用户身份针对 live host 驱动运行中插话的通道,考虑到这里的 acp-integration 接线是新增的,它比平时更重要。两者都不能替代先把构建修好——而且在这个 PR 改为面向 main 或 release/** 分支之前,ci.yml 会一直完全不运行。
— Qwen Code · qwen3.8-max-2026-09-02
Reviewed at a3146b0487982b8cbc9411988156a7515015fa6a · re-run with @qwen-code /triage
|
Confidence: 2/5 — the design is good and I could follow every argument it makes, but it does not compile, and that is not a judgement call. Stepping back: the mid-run steering half is the best-reasoned part of this stack. I wrote my own proposal before opening the diff and the implementation matched it, then beat it on reuse — it wires into The question is not that, though. That is the thing I would want a human to weigh, beyond the one-line fix. This PR started as a focused 8-file change and is now a 16-file, two-package bundle that has absorbed #11263's supervisor wholesale and what looks like #11261's session-list fix, while its body still says "Stacked on #11263" and still describes only the three original changes. Those two PRs are still open beside it. Nobody reading the description can tell what is actually under review, and the diff now touches Six months from now I would thank the author for the comments in this code; they explain why acceptance is not consumption and why the terminal write is the backstop, which is the part a future maintainer actually needs. I would also want the stack to stop absorbing its own neighbours silently. What would move this to an approve: fix the Requesting changes on the build break. Everything else is discussion. 中文说明Confidence: 2/5 —— 设计是好的,它提出的每一个论点我都能跟上,但它编译不过,而这一点没有商量余地。 退一步看:mid-run steering 这一半是整个堆叠里论证最扎实的部分。我在打开 diff 之前先写了自己的方案,实现与之一致,而且在复用上做得比我的方案更好——它接入了既有的 但问题不在这里。 这才是我希望人来权衡的地方,而不只是那一行修复。这个 PR 最初是一个聚焦的 8 文件改动,现在是 16 文件、跨两个包的打包,已经把 #11263 的 supervisor 整个吸收进来,还带上了看起来属于 #11261 的 session-list 修复,而它的描述仍然写着「Stacked on #11263」、仍然只描述原来的三项改动。那两个 PR 还在旁边 open 着。任何人读描述都无法判断实际被审查的是什么,而 diff 现在触及了 六个月后我会感谢作者写在这些代码里的注释;它们解释了「为什么接受不等于消费」、「为什么终态写入是最后的兜底」,而这正是未来维护者真正需要的部分。我也会希望这个堆叠不要再静默地把自己的邻居吸收进来。 能把它推向 approve 的是:修掉 以编译失败为由请求修改。其余都是讨论。 — Qwen Code · qwen3.8-max-2026-09-02 Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Needs a fix before this can go anywhere — packages/core does not compile.
run-lifecycle.ts:36 imports { Thread, ThreadEvent, ThreadMessage } but the new rebookUnconsumedTriggers uses ThreadRun at lines 398 and 414, so tsc fails with six errors (two TS2552, four cascading TS7006 under noImplicitAny). Adding ThreadRun to that import should clear all of them. Both tui-parity jobs hit this independently on a3146b04.
The reason it slipped through is worth noting: npx vitest run src/agents/mesh/ transpiles with esbuild and never typechecks, so 144 green tests and a broken build are both true. Please confirm npm run build && npm run typecheck across packages/core and packages/cli — the build died in core, so the CLI half (acpAgent.ts, serve/server.ts, session-list.ts) has never been compiled by any gate.
Second thing, not a blocker but it needs settling before anyone can review this honestly: the body still describes the earlier, narrower PR. It says "Stacked on #11263" and lists three changes, while the diff has absorbed #11263's supervisor.ts outright, picked up what looks like #11261's session-list.ts fix, added the ACP and daemon wiring, and grown from 8 files to 16 across two packages. Both of those PRs are still open beside this one.
The design itself is good — I traced the acceptance-is-not-consumption chain through runtime-bridge.ts and it holds. Full notes, including the untested drain path and four non-blocking observations, are in the review comment above.
中文说明
这个 PR 需要先修一处才能继续——packages/core 编译不过。
run-lifecycle.ts:36 导入的是 { Thread, ThreadEvent, ThreadMessage },但新增的 rebookUnconsumedTriggers 在 398 行和 414 行用到了 ThreadRun,于是 tsc 报出六条错误(两条 TS2552,四条在 noImplicitAny 下连带产生的 TS7006)。把 ThreadRun 加进那条 import 应该就能全部消除。tui-parity 的两个作业都在 a3146b04 上独立撞到了这个问题。
它之所以漏过去,原因值得记一下:npx vitest run src/agents/mesh/ 用 esbuild 转译、从不做类型检查,所以「144 条测试全绿」和「构建失败」两件事同时为真。请确认 npm run build && npm run typecheck 在 packages/core 和 packages/cli 上都通过——构建死在 core,所以 CLI 那一半(acpAgent.ts、serve/server.ts、session-list.ts)从未被任何门禁编译过。
第二件事,不是阻塞项,但在任何人能够诚实审查之前需要先理清:PR 描述仍然写的是那个更早、更窄的版本。它写着「Stacked on #11263」并列出三项改动,而 diff 已经把 #11263 的 supervisor.ts 整个吸收进来、带上了看起来属于 #11261 的 session-list.ts 修复、新增了 ACP 与 daemon 接线,并从 8 个文件跨两个包增长到 16 个文件。这两个 PR 都还在旁边 open 着。
设计本身是好的——我沿着「接受不等于消费」这条链路追过了 runtime-bridge.ts,它是成立的。完整意见(包括没有测试覆盖的 drain 路径和四条非阻塞观察)见上方的审查评论。
— Qwen Code · qwen3.8-max-2026-09-02
Reviewed at a3146b0487982b8cbc9411988156a7515015fa6a · re-run with @qwen-code /triage
What this PR does
Moves direct running delivery and unconsumed-trigger reconciliation out of step 8 and into the working system. Three changes: the dispatcher pushes pending triggers into a running body before it looks for idle work;
acceptRunDeliveryrecords that the runtime queue took them; and the terminal write rebooks anything the run was told to answer and never read.Stacked on #11263.
Why it's needed
A post into a running turn was being silently swallowed. Admission returns
coalesce(running)for it, appends it to the live run'striggerMessageIds, and charges a turn. Nothing then delivered it —queueExternalInputhad no caller anywhere in mesh — and nothing rebooked it:finishRunhad no reconciliation, so when the run ended the message was gone. That is the single failure mode this design says it will not have, and it sat on the path the demo is built around: "the person interjects mid-run" is step 4 of §8.It is also the one thing this system does that Multica cannot. §0.1 rests on it, and §7's comparison table names it as the only place we are ahead. Leaving it in step 8, behind restart recovery and outbox replay, meant shipping the claim before the capability.
Acceptance is not consumption. The queue taking the input says nothing about the model having read it, so
acceptRunDeliverywritesacceptedMessageIdsand extends the run's context window, and deliberately does not touchconsumedMessageIdsor the delivery watermark. The correlated drain event inruntime-bridge.tsis still what commits, matched by delivery id rather than guessed at from the text.The terminal write is the backstop, because it is the last moment at which "this run will never read it" becomes true. A refused delivery therefore costs latency and never a message, which is what makes the at-least-once contract in decision 9 real rather than aspirational.
Only human and system triggers are replayed. An agent-authored post that missed has its author still on the thread, and the turn gate exists precisely to stop two agents re-triggering each other, so replaying one would spend budget repeating a conversation nobody is waiting on.
Reviewer Test Plan
How to verify
cd packages/core npx vitest run src/agents/mesh/New cases: steering a running agent (delivery id is the run id; accepted but not consumed; never delivered twice); a refused delivery recorded as
delivery_raceand rebooked at finish; a consumed trigger not replayed; an agent post not replayed while the human post beside it is; no rebook onto a thread a person closed.Evidence (Before & After)
Before: post into a running turn → charged, recorded on the run, never delivered, never rebooked. After: delivered at the next tool boundary, or rebooked when the runtime refuses.
Tested on
Environment (optional)
Named Vitest files only. Observed:
src/agents/mesh/14 files / 144 tests (was 139).Risk & Scope
deliveris optional on the port, so a future runtime that cannot reach a live body degrades to "wait for the next run" rather than failing. The rebook path covers it either way.runtime-bridge.ts's existing correlation, not by a new test here.runtime-bridge.ts:100tripsno-unsafe-finally(athrowinsidefinally, from feat(mesh): bind dispatcher runs to agent turns #11252). It is one of the reasons Lint is red on feat(mesh): add persistent shared-thread agent collaboration #11206 and is left for a focused fix rather than mixed in here.Linked Issues
Parent delivery PR: #11206. Stacked on #11263. Implements the mid-run half of decision 9 and §4's
accepted_running/delivery_race.