feat(goal-channel): add botmux runtime integration - #3348
Conversation
Bind a verified botmux bot to a LoopX goal, provide dry-run setup/doctor/trigger/status/disable commands, and preserve at-most-once dispatch receipts under a goal-scoped file lock. Signed-off-by: wangyuchao.404 <wangyuchao.404@bytedance.com> Co-authored-by: TRAE CLI <noreply@bytedance.com>
Document activation, operation, failure semantics, and rollback for the runtime-neutral botmux Goal Channel integration and expose it through the integration index and site navigation. Signed-off-by: wangyuchao.404 <wangyuchao.404@bytedance.com> Co-authored-by: TRAE CLI <noreply@bytedance.com>
c1e2b6e to
5c6a5a2
Compare
huangruiteng
left a comment
There was a problem hiding this comment.
Exact head: 5c6a5a21862975ada3b4819c98f2a543b1b33d4e
动机
这个 PR 想把既有 botmux 作为 Goal Channel 的可选运行时:LoopX 负责 canonical Goal/Todo/Gate/Quota 状态,botmux 只负责 IM 投递、持久 agent session 和运行结果观察。用户可通过 goal-channel runtime setup|doctor|trigger|status|disable 完成绑定、健康检查、显式触发、状态读取和回滚。这个需求有真实调用点,CLI 入口也完整;相较于让用户手工拼 botmux HTTP 请求,本 PR 将项目工作目录、chat membership、私密凭据名、幂等 receipt 与 LoopX authority boundary 收拢成一个可审计流程。
但 exact-head 实现里有两处关键失败路径没有兑现公开契约:离线 bot 会被报告为 online;provider 调用结果未知时,返回状态、持久 receipt 与后续 status 三者互相矛盾。因此当前不能批准进入 main。
改动思路
CLI 在 loopx/cli_commands/goal_channel.py 注册五个 runtime 子命令,并先通过 _source_context() 定位 Goal 的 canonical source registry,再把调用交给 provider-specific botmux_runtime.py。setup/doctor 读取 botmux health、bots、groups 和 dry-run trigger;execute setup 用 owner-only atomic JSON 保存本地私密 binding。trigger 根据 Goal active-state 内容或显式 turn key 生成 dispatch key,在 goal-scoped file lock 内先写 attempting receipt,再调用 botmux;后续 turn 复用 session 与 provider turnIdempotencyKey。status 只公开归一化 state 和 output-available 布尔值,disable 只停用当前 Goal binding。
正向路径设计合理:有效 bot/chat/project route -> 0600 binding -> preview -> 单次 dispatch -> queued session receipt -> running/completed/failed/not_found observation;所有 provider ids、token、raw output 都留在 local-private state。负向路径本应 fail closed,但 _probe_botmux() 和 _trigger_botmux_runtime() 的状态转换缺口会让 operator 得到错误的 ready/idle 结论,详见下方 findings。
具体改动
- 生产代码:
botmux_runtime.py新增 1,161 行,负责 validation、HTTP transport、private binding、probe、dispatch receipt、status 与 locking;goal_channel.py新增 154 行,负责 CLI 注册和路由。 - 测试:
test_botmux_goal_channel_runtime.py新增 755 行,覆盖 dry-run/setup、0600 权限、route/working-dir、idempotency、session reuse、rebind、四态 status、terminal failure 与并发 trigger。 - 文档/配置:新增 137 行操作文档,RFC 增补 authority boundary,集成索引与
mkdocs.yaml增加导航。无 generated 文件。
关键代码讲解
_probe_botmux():读取 health/bots/groups,再用 dry-run trigger 验证 route。它是 setup/doctor 的唯一 readiness authority;因此这里的任何误判都会把不可运行的 provider 写成configured/ready。_write_private_json_atomic()+ public packet guard:临时文件先设 0600、flush/fsync 后 atomic replace;公开响应递归禁止私有 key,避免 bot/chat/session/token 进入 CLI packet。这个边界是本 PR 做得最扎实的部分。_trigger_botmux_runtime():dispatch key 由 goal/revision/instruction 决定,lock 内先写attempting,成功后写queuedsession,API rejection 写rejected。这里也是第 2 个 blocker 的状态机断点。_status_botmux_runtime():只有 binding 中存在session_id + active_dispatch_key才查询 provider;否则直接返回idle。这与未知首次 dispatch 没有 session receipt 的情况发生冲突。handle_goal_channel_command():从 source registry 解析 local-private binding,并把 ValueError 收敛成 public-safeinvalid_configuration;provider payload 不直接泄露到 CLI。
状态规则目前用散落字符串集合表达(attempting/queued/running/completed/failed/not_found/unknown/rejected),而不是一个 typed enum/transition helper。第 2 个 finding 正是这种分散状态写入导致的真实错配;修复时应顺手把合法 transition 收拢为单一 helper,避免只补一个分支后继续留下另一条 invalid-response 分支。
对主干的风险
[P1] setup/doctor 将明确离线的 bot 报告成 online
- Trigger:
/api/bots返回匹配larkAppId,但该项online=false。 - Code path:
loopx/control_plane/goals/botmux_runtime.py:347-357只检查“存在匹配项”,从未检查online;随后_probe_botmux()无条件返回"bot_online": True。 - Risky outcome:setup/doctor 返回 ready/configured,用户随后才在真实 dispatch 失败;这违反 PR summary 与操作文档“确认 selected bot is online”的公开契约。
- 独立复现:构造
online=false、其余 route 合法的 provider response,exact head 返回ok=true,status=preview_ready,details.bot_online=true。 - Minimum repair:按 botmux 的 canonical online 字段 fail closed(至少要求显式
online is True),新增 offline bot negative test;不要把字段缺失也当 online,除非文档明确兼容协议并有可验证替代信号。
[P1] dispatch_unknown 没有写进 receipt,后续 status 反而报告 idle
- Trigger:首次 dispatch 在 provider 可能已收到请求后发生 transport failure,或 response 缺少可验证 session id。
- Code path:line 800 先持久化
attempting;line 830-844 与 879-890 返回dispatch_unknown,但没有像 API rejection/queued 分支那样更新并保存 receipt。首次 dispatch 又没有 session pointer,所以 line 943-955 的 status 直接返回idle。 - Risky outcome:同一事件出现“CLI 返回 dispatch_unknown、磁盘仍 attempting、status 说没有 dispatch”的三套事实;operator 无法通过公开 status 判断该不该人工核查,并且 PR body 声称的“shutdown persisted dispatch_unknown”并未实现。
- 独立复现:让 requester 抛
BotmuxTransportError后,返回状态为dispatch_unknown,binding receipt state 为attempting、session 为空,紧接着 status 为idle。 - Minimum repair:用 typed transition helper 将 transport/invalid-receipt 两条分支原子持久化为
unknown(保留 attempted_at/receipt id),让 status 在没有 session 时先检查 active/latest unknown receipt并公开unknown,同时新增 transport failure、invalid receipt、duplicate retry 和 status readback regression tests。
除了这两个 blocker,provider-specific 1,161 行实现直接放在 control_plane/goals/ 会提高 core change coupling。当前 CLI 是真实 active caller,所以不是 unused scaffolding;但建议后续把 bundled Botmux provider放到明确 extension/provider boundary,保留 provider-neutral Goal Channel caller contract。该 placement 建议本轮不单独作为 blocker。
独立验证:focused pytest 13 passed;changed-file Ruff 通过;三文件 py_compile 通过;GitHub checks 9 个成功/跳过发布动作,无 pending。验证覆盖了正向 setup/trigger/status、并发 at-most-once 与多个负向配置场景,但没有覆盖上述 offline/transport-unknown 两条关键失败路径。
我的整体评价
结论:REQUEST_CHANGES。authority/privacy、0600 atomic binding、goal-scoped lock、session reuse、public packet redaction 和 opt-in/rollback 文档整体方向正确,测试量也与新运行时表面相称;但 readiness 与 dispatch receipt 是这套能力最核心的两个安全边界,现在都存在可复现的错误结论。请先修复上述两项并把 dispatch state 收敛成 typed transition contract,再在新 exact head 上重跑 focused tests、negative repro、Ruff、canary 与远端 checks,我会优先复审。
English verdict: REQUEST_CHANGES at exact head 5c6a5a21862975ada3b4819c98f2a543b1b33d4e. An explicitly offline bot is reported as online/ready, and transport or invalid-receipt failures return dispatch_unknown without persisting that state, after which status incorrectly reports idle. Focused tests (13), Ruff, py_compile, and GitHub checks pass, but the two missing negative paths are blocking.
Fail closed for explicitly offline bots, centralize dispatch receipt transitions, persist unknown provider outcomes, and surface unresolved attempts through runtime status instead of reporting idle. Signed-off-by: wangyuchao.404 <wangyuchao.404@bytedance.com> Co-authored-by: TRAE CLI <noreply@bytedance.com>
|
Addressed both P1 findings at exact head Changes:
New negative coverage includes:
Validation on the new head:
Please re-review the new exact head. |
Summary
goal-channel runtime setup|doctor|trigger|status|disablefor an optional, runtime-neutral botmux integrationIssue Or Task
Validation
python3 -m py_compile loopx/cli_commands/goal_channel.py loopx/control_plane/goals/botmux_runtime.py tests/control_plane/test_botmux_goal_channel_runtime.pyloopx check --scan-path ...for all seven changed public files (0 findings)online=falseblocks setup before chat probingunknown, status never reports idle, and duplicate retry does not re-dispatchpython -m ruff check tests loopx/canary loopx/control_plane loopx/domain_packs loopx/presentation loopx/cli_commands/goal_channel.pypython examples/control_plane/cli-output-budget-regression-smoke.pypython examples/canary/smoke-suite-runner-smoke.pyloopx canary premerge --from-git-diff: 18/18 selected checks passed, no manual holdsThe broad repository pytest run reported 16 environment/known unrelated failures with 3355 passed and 2 skipped. Re-running the failure set with an absolute source
PYTHONPATHreduced it to 4 environment failures: two retained-case tests because this host has/tmp/.git, and two onboarding tests that deliberately replace the subprocess environment and cannot import an uninstalled checkout. The host also lacksensurepip, which blocks the extension-scaffold venv test.Provider observation hold: in the real core-only protocol probe, the configured agent produced the requested output, but botmux's
trigger-resultdid not converge before timeout and shutdown persisteddispatch_unknown. The LoopX integration preserves that fail-closed observation and does not infer a canonical state transition from message delivery or model output.Type of Change
LoopX Area
Technical Direction
Core control-plane hardening
Long-horizon benchmark evidence
Operator surface and IM integration
Shared Goal Authority and cross-host coordination
Architecture and research incubator
Target base branch:
mainDirection tracker or promotion unit: Goal Channel collaboration v0 / Agent IM boundaries
Boundary Checklist
.loopx/,.codex/goals/, liveACTIVE_GOAL_STATE.md, credentials, private benchmark traces, verifier output, raw agent sessions, internal document links, or local machine paths.Signed-off-bytrailer (git commit -s).