feat(cli): add ACP tools, interactions and session-scoped MCP - #5222
feat(cli): add ACP tools, interactions and session-scoped MCP#5222Sun-GLiang wants to merge 4 commits into
Conversation
9fe25e8 to
ea42a67
Compare
Project tool activity and reconcile authoritative results before completing ACP prompts. Route standard interactions through Host authority and publish isolated stdio MCP capabilities per Session using the shared TUI publication queue. Refs apache#3132 Generated-by: Codex
ea42a67 to
e8c019f
Compare
hqhq1025
left a comment
There was a problem hiding this comment.
Technical NO-GO on the current head: one P1 and two P2 findings remain in the new ACP interaction and Session-scoped MCP lifecycle.
I reviewed the full 45-file change, including the ACP prompt/interaction path, scoped capability registration and invocation, reconnect handoff, MCP process ownership, cancellation, transcript projection, and the new tests. Local verification passed build:test, full typecheck/lint/format, ASF headers, git diff --check, CLI 1112 passed / 3 skipped, focused CLI interaction/MCP/transcript tests 162/162, focused Runtime Host/Core capability tests 49/49, renderer architecture 112/112, and the E2E budget (36 tests). The hosted test job is red on an unchanged Runtime code-mode timing assertion that passed in the focused local rerun; all package and installed-CLI jobs passed.
The branch is also currently unmergeable with main 4cd71eaed26dbf296f1142db3296b27136d7d131 because packages/cli/src/acp/session-registry.ts conflicts.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
| } | ||
|
|
||
| cancelTurn(turnId: string): void { | ||
| this.#cancelledTurnId = turnId; |
There was a problem hiding this comment.
[P1] Preserve cancellation for every unresolved Turn
cancelTurn() overwrites a single #cancelledTurnId, while AcpSessionRegistry calls it unconditionally from every prompt's finally. With two overlapping prompts, cancelling Turn A and then letting Turn B finish/return blocked replaces A with B. A delayed or replayed pending interaction for A then passes the check at lines 102-104, reopens the client dialog, and can submit interaction.answer for the already-cancelled Turn.
A compiled registry-level reproduction held A's Stop unresolved, completed B as active_turn, replayed A's pending question, and observed both dialogs=1 and an interaction.answer carrying A's interaction ID. This can authorize or continue tool work after the user cancelled it. Track cancellation per unresolved Turn (and retire entries only when their interaction/Turn becomes terminal), and add the concurrent A-cancel/B-finish/replay regression.
There was a problem hiding this comment.
Fixed in 514589d, included in the current PR head 1882926. Cancellation is now retained independently for every Turn in a per-Turn map. Ordinary prompt settlement uses a separate settleTurn path, so Turn B completing or being blocked cannot replace or clear the cancellation fence for Turn A. A cancelled interaction remains fenced until its terminal resolution retires it. I also added the registry-level regression a second prompt settling cannot reopen a cancelled Turn interaction replay; it holds the Stop for A, lets B return blocked, replays the pending interaction for A, and verifies that no Host query, client dialog, or interaction.answer is emitted.
已在 514589d 修复,并包含于当前 PR head 1882926。取消状态现在按 Turn 独立保存;普通 prompt 收尾改走单独的 settleTurn 路径,因此 Turn B 完成或被阻止时,不会覆盖或清除 Turn A 的取消隔离。被取消的 interaction 会持续受保护,直到其终态解析将其退休。同时新增了 registry 级回归测试 a second prompt settling cannot reopen a cancelled Turn interaction replay:保持 A 的 Stop 未完成,让 B 返回 blocked,再重放 A 的 pending interaction,并验证不会发起 Host 查询、客户端弹窗或 interaction.answer。
| if (previousConnection) previousConnection.superseded = true; | ||
| this.#invocations.releaseConnection(previousConnectionId); | ||
| if (registration.sessionId !== undefined) { | ||
| provider.sessionRegistrations.set(registration.sessionId, registration); |
There was a problem hiding this comment.
[P2] Retire the Session-scoped registration with the Session
This new map entry remains the current scoped registration when the Host archives or removes the Session: retireSessions() deletes only the derived #sessions binding state, so #releaseRegistrationIfUnused() cannot release this record and the client receives no registration_release.
A coordinator reproduction published a scoped tool, bound the Session, retired it, and observed zero release frames; binding the same retired Session ID again still succeeded and exposed the tool. For ACP this also leaves the Session MCP manager/process alive unless the separate ACP session.close or whole-connection cleanup happens. Remove and release the scoped registration when the Host retires the Session, with archive/remove coverage.
There was a problem hiding this comment.
Fixed in 514589d, included in the current PR head 1882926. retireSessions now removes the current Session-scoped registration from the provider, advances the capability revision, invalidates model tools when applicable, and releases the registration as soon as snapshot and invocation leases are idle. This emits client.capability.registration_release to the owning client and prevents the retired Session ID from exposing the old tool after a later bind. Coverage exercises both archive- and remove-labelled Session retirement and verifies both release frames plus the absence of tools after rebinding.
已在 514589d 修复,并包含于当前 PR head 1882926。retireSessions 现在会从 provider 中移除当前 Session scoped registration、推进 capability revision、在适用时触发模型工具失效,并在 snapshot 与 invocation lease 空闲后释放 registration。这样会向所属客户端发送 client.capability.registration_release,同时确保已退休的 Session ID 后续重新 bind 时不会再次暴露旧工具。测试覆盖了 archive 和 remove 两类 Session 退休,验证两条 release frame 均已发送,并验证重新 bind 后不再存在对应工具。
| scopedPrevious.connectionId !== context.connectionId && | ||
| this.#registrationConnection(scopedPrevious) | ||
| ) { | ||
| throw new Error('Session capabilities are already published by another live connection'); |
There was a problem hiding this comment.
[P2] Allow the reconnecting owner to take over its scoped registration
A reconnect publishes the new connection as available immediately, while the old server connection releases its capabilities asynchronously. During that overlap, this branch rejects the same provider's new Session-scoped replacement as “another live connection”; the global path below explicitly supersedes the old connection, but the scoped path has no equivalent handoff.
A controlled coordinator sequence accepted the old registration, rejected the new connection until the old close completed, and accepted the identical replacement afterwards. In ACP, that rejection leaves McpCapabilityPublication in error, so a prompt racing the handoff fails mcp_publication_failed; a later prompt can recover only by initiating another ready() attempt. Treat an authenticated reconnect of the same provider as a takeover, or wait/retry through the old connection's release, and cover the overlap deterministically.
There was a problem hiding this comment.
Fixed in 514589d, included in the current PR head 1882926. A live authenticated connection for the same provider can now replace the current Session-scoped registration without waiting for the stale connection to close. The Session slot is switched to the new registration atomically; existing snapshots retain their immutable old lease, while the old registration receives registration_release once those snapshot or invocation references become idle. The deterministic test keeps the old connection live, publishes the same Session scope from the reconnecting connection, verifies that new snapshots use the new owner, and verifies release of the old registration.
已在 514589d 修复,并包含于当前 PR head 1882926。同一 provider 的已认证新连接现在可以直接替换当前 Session scoped registration,无需等待旧连接先关闭。Session slot 会原子切换到新 registration;已取得的 snapshot 仍保留不可变的旧 lease,而旧 registration 会在 snapshot 或 invocation 引用空闲后收到 registration_release。确定性测试保持旧连接存活,由重连连接发布同一 Session scope,验证新 snapshot 已使用新 owner,并验证旧 registration 随后被释放。
# Conflicts: # packages/cli/src/acp/session-registry.ts
hqhq1025
left a comment
There was a problem hiding this comment.
Technical GO on exact head 1882926bbbffc9972a872c6f7b323eead56582e8; I found no remaining P0-P3 issues.
The three prior lifecycle findings are fixed. Cancelled interaction state is now isolated per Turn and retained until that prompt settles, so another prompt cannot reopen a delayed interaction. Session retirement removes and releases scoped capability registrations. An authenticated reconnect can replace the same Session scope without waiting for the old connection's asynchronous release, while old-connection cleanup is fenced from deleting the replacement. I also checked the merge resolution that keeps Stop delivery pending through prompt teardown.
Verification passed build:test, full typecheck/lint/format, ASF headers, git diff --check, CLI 1114 passed / 3 skipped, focused ACP/interaction/capability tests 161/161, renderer architecture 112/112, the E2E budget (38 tests), and all hosted checks. Runtime Host full tests were 1924 passed / 19 skipped / 1 failed; the sole failure is the unchanged managed Bash sandbox integration because this runner rejects both unshare and bwrap. The head directly includes current main 4cd71eaed26dbf296f1142db3296b27136d7d131 and is mergeable. I did not exercise a real external ACP client or MCP server, or native Windows/macOS interaction paths.
Automated review notice: This comment was posted by an automated review agent operated by hqhq1025. It is not an independent human review and does not replace one.
Summary
ACP Sessions can now display tool activity, deliver authoritative tool results before prompt completion, answer standard questions/forms and permission requests, and run isolated stdio MCP tools. A Session's MCP registrations, grants and processes remain separate even when another Session uses the same tool names.
This implements PR5 from the ACP checklist. It reuses the existing Session channel/projector, bounded output buffer, MCP manager/provider factory, form continuation and Host grant paths, and shares MCP publication scheduling with TUI.
Refs #3132
Refs #4862
Verification
6dfec43fe, which contains the merged feat(cli): add ACP live session lifecycle #4862 prerequisite.npm run buildandnpm run typecheck; CLI third-party notices; protocol epoch, ASF header and whitespace checks; Desktop E2E budget; Desktop and UIknipchecks.npm run lint(3570 files) andnpm run format:check(2115 files).askpermission to final result; typed modern MCP form continuation; parallel Session isolation; cancellation with a client that never replies; and reconciliation with all 16 subscription slots occupied. The last case also verifies that a seventeenth subscription is rejected.Review focus
The branch now starts directly at current Apache main
6dfec43fe. PR5 is isolated in the single commite8c019f71.Host compatibility epoch advances 151 → 152. The new Session scope and MCP admission fields require compatible Client/Host builds; grant storage needs no SQL migration. Load/resume, full MCP configuration replacement and HTTP/SSE/OAuth management remain outside PR5.
Zed acceptance
tool_search, the Session-scopedfixture/echopermission card, the completedechocard, and the final assistant response. The captured ACP stream records the selected allow option, authoritativeresultPending: falseresultZed PR5 MCP result verified, and finalend_turn. Full evidence is in VALIDATION.md.AI use
Tool(s) and scope: Codex contributed implementation, tests, documentation and this PR description. The affected commits carry
Generated-by: Codex; retain the trailer in the final squash commit. AI checks do not replace the required independent human review.Checklist
Does this PR entail a change in behavior?