「Your pull request has been closed」
2026-04-26 早上打開 GitHub,看到一封 12 天前送出的 PR 來自 maintainer 的最新通知:
Your pull request has been closed.
State: CLOSED
mergedAt: null
第一反應是嘆口氣——卡了 6 輪 bot review,最後還是被關了。準備重新開一個 issue 解釋為什麼這個方向是對的。
但點進去看留言才發現完全不是那回事。
12 天前發生了什麼
04-14 半夜我在用自己跑在 VPS 上的 AI agent,那個 agent 走 OpenClaw 串接 Discord。每個 Discord thread 可以綁一個 ACP(Agent Client Protocol)session 跟 Claude/Codex/Gemini 等 CLI agent 對話。
用到一半發現一個怪現象:在綁定 thread 內打 /acp close,原本應該關掉 ACP session,但卻被 ACP agent 當成「請求」吃掉,回了一段莫名其妙的對話。/status、/unfocus 也一樣。
trace 進 OpenClaw commands-acp.ts 發現 dispatch 路由跳過了 handleAcpCommand:在 ACP-bound 的 conversation 裡,所有 text 都被當 prompt 送給 ACP harness,連 /acp 開頭的管理指令都不例外。
當下一氣之下做了三件事:
- 03:14 開 issue #66298
- 07:46 fork → 寫修補 → 開 PR #66407
- 走完整 CONTRIBUTING checklist:
pnpm tsgo/pnpm check/pnpm build/ targetedpnpm vitest全綠
PR diff 不大:2 個檔,+138/-13。在 commands-acp.ts 的 dispatch 點加 bypass,/acp 開頭的 text 不再被吞進 ACP harness、轉回 Gateway 命令處理路徑。
6 輪 bot review,最後卡在 scope-out
OpenClaw 跑 Codex Review + Greptile bot,每次 push 都會自動重 review。前 5 輪都是常見的 P1/P2(path normalize、case 處理、regression coverage、bypass 跟 handler 同步),改完都收。
真正卡住的是第 6 輪:Codex 抓到 commands-acp.ts:90 一個 pre-existing startsWith bug——這個 bug 跟 PR 主題相關但 scope 之外(在更下層的 normalizeCommandBody,應該另開 issue)。我留言 scope-out 貼了 trace 連結 discussion_r3078655259,這條 unresolved 之後 maintainer 沒回應,狀態維持了 12 天。
中間還發生兩件事:
- 04-18:第三方
joeia26開了 sibling PR #68617 解同一個 bug,diff +47/-2 比我小很多 - 04-21:openclaw 的
prtags-bot把這兩個 PR 列為great-loon-t2teduplicate group
看到 sibling PR 有點挫敗——會不會別人先被 merge?但他也卡著沒動。
04-26 凌晨那 5 分鐘
對照 timestamp 看(GitHub API 都精準到秒):
01:58:19 steipete 推 commit a6d9926d1d 上 main
02:02:46 Issue #66298 closed (stateReason: COMPLETED)
02:02:53 PR #66407 closed (mergedAt: null) ← 我這個
02:02:59 PR #68617 closed (mergedAt: null) ← sibling
maintainer 先 commit 上 main,4 分鐘後回頭把 issue + 兩個 PR 一次關掉。三個 close 動作差不到 13 秒。
關 PR 的留言是這樣寫的:
Fixed on
mainin https://github.com/openclaw/openclaw/commit/a6d9926d1d, following this PR’s direction and adding the missingcommands.text=falseescape-hatch coverage.The landed fix keeps
/acp ...local in ACP-bound conversations, keeps/acp closeworking even when text commands are disabled, preserves the existing auth/scope checks, and also documents the bound-session command routing rule. Verified withpnpm check:changed.Thanks @kindomLee; the changelog credits your work here. Closing this as superseded by the main-branch fix.
而 CHANGELOG.md line 482,在 ## 2026.4.25 → ### Fixes 段:
- ACP: keep `/acp` management commands, plus local `/status` and `/unfocus`,
on the Gateway path inside ACP-bound threads so they are not consumed as
ACP prompt text. Fixes #66298. Thanks @kindomLee.
這是「形式上 closed superseded、實質上 fix landed + 名字進 changelog」的 outcome。
maintainer 做的事跟我做的事
我的 PR:2 檔 / +138/-13。在 commands-acp.ts 的 dispatch 點 inline 修。
maintainer 的 commit a6d9926d1d:9 檔 / +199/-8。
| 檔案 | 用途 |
|---|---|
CHANGELOG.md | +3 行(我的 credit 在這) |
docs/tools/acp-agents.md | +6 行(新文件) |
docs/tools/slash-commands.md | +5 行(新文件) |
src/auto-reply/reply/commands-acp.ts | +1 行(主修點) |
src/auto-reply/reply/dispatch-acp-command-bypass.ts | +16 行(抽成獨立 module) |
src/auto-reply/reply/dispatch-acp-command-bypass.test.ts | +128 行(regression test) |
src/auto-reply/reply/commands-acp.test.ts | +18 行 |
scripts/test-projects.test-support.mjs + test/scripts/test-projects.test.ts | +22 行(測試 infra) |
兩件事值得注意:
- 方向完全一樣:把
/acp開頭 text 從 ACP harness 路由抽回 Gateway 命令路徑——maintainer 留言裡寫的 “following this PR’s direction” 不是客套 - scope 擴大:我只處理
/acp,他擴到/status/unfocus也本地化、且加上commands.text=false的 escape hatch(即使 text commands 整個被關,/acp close仍能逃生) - 架構升級:我 inline 在
commands-acp.ts內改;他抽成獨立dispatch-acp-command-bypass.tsmodule + 128 行 regression test + 兩份 docs
第 6 輪卡住的那個 P2 scope-out 議題(pre-existing startsWith bug),他在新 module 裡乾脆繞過去了,不需要碰原本那條 path。
closed without merge ≠ rejected(至少這次不是)
從 GitHub UI 看:state = CLOSED、mergedAt = null、頁面頂端紅色「Closed」badge。看起來就是被拒絕。
但同時:issue stateReason = COMPLETED(不是 NOT_PLANNED)、CHANGELOG 寫了 credit、commit message 說 “following this PR’s direction”。closedByPullRequestsReferences 也是空的——因為不是被另一個 PR merge 關掉,是 maintainer direct commit 上 main 後手動關。
這次的 outcome 形狀大概是:maintainer 吸收 fix direction、重寫成更乾淨的架構(抽 module / 加大量測試 / 加 docs)後 commit 上 main、回頭把原 PR close 為「superseded」。
我不知道這在 OSS 是不是常見 pattern——這是我第一次貢獻大專案,樣本數 1。但至少這次,PR 作者第一眼看到「closed, mergedAt: null」就下「我的 PR 被否決了」結論的話,會錯過真實 outcome。
下次再看到 closed/mergedAt=null 之前,跑這幾步
不下結論之前,先收集資料:
# 1. close 留言:maintainer 通常會說明 next steps 或 superseded path
gh pr view <n> --comments
# 2. 連動 issue 的 stateReason 才是真實意圖(COMPLETED ✅ vs NOT_PLANNED ❌)
gh issue view <issue_number> --json state,stateReason,closedAt
# 3. 看 close 時間附近 main 的相關 commit
gh api "repos/<owner>/<repo>/commits?since=<close_date_minus_1d>" \
--jq '.[].commit.message' | head -20
# 4. release notes / CHANGELOG 看有沒有 credit
grep -i "<your_username>" CHANGELOG.md
gh release list --repo <owner>/<repo> --limit 3
最後一招特別有用:grep -i "<your_username>" CHANGELOG.md 跟看最新幾個 release notes。maintainer 採用你方向的話,常見會用 Thanks @<username> 形式 credit。沒被 credit 跟有被 credit 是兩種完全不同的 outcome。
收尾
這 12 天我做的事:
- 開 issue + PR(4.5 小時,bug trace 跟 fix 都找對了方向)
- 走完 6 輪 bot review(耐性消耗最大的部分)
- 卡在 scope-out 第 6 輪 12 天無人回應(這段最痛)
- 04-26 凌晨醒來看通知以為被 reject
最後得到的:fix 在 v2026.4.25 release 上線、CHANGELOG 名字、ACP-bound thread 內三個指令(/acp /status /unfocus)正式不會再被 ACP harness 吞掉。
對我來說的 takeaway:「貢獻」這次不是 PR commit hash 出現在 git log 裡,是 maintainer 用我的 direction 寫得更好、回頭把 credit 給我。 形式上不漂亮,實質上 ship 了我做不到的 scope(/status /unfocus 一起本地化)跟 test coverage。
這條 lesson 寫進自己的 LEARNINGS:closed, mergedAt: null 是訊號不是結論,去查 close 留言 + issue stateReason + main commit + CHANGELOG 四件事再下判斷。
