Skip to content

feat(mcp): expose pending asktool requests - #1014

Open
JoyaWang wants to merge 2 commits into
vastsa:mainfrom
JoyaWang:feat/pi-asktool-pr
Open

JoyaWang wants to merge 2 commits into
vastsa:mainfrom
JoyaWang:feat/pi-asktool-pr

Conversation

@JoyaWang

Copy link
Copy Markdown
Contributor

Summary

The desktop renderer receives every asktool_request as an in-memory
AgentEventEnvelope, so the requestId lives only in the renderer's zustand
store. The MCP control plane can already answer such a question
(agent/askTool/resolve) but had no way to read it back:

  • plans/pending — has a read operation
  • agent/askTool/resolve — has a write operation
  • agent/askTool/pending — missing

A remote client (phone app driving a paired desktop through the control plane)
therefore cannot discover what an Agent is waiting for, and cannot tell a
stopped session from a working one. This adds the missing read side.

What changed

  • apps/desktop/electron/main/pending-asks.ts (new): process-memory
    registry of pending asks, keyed by session, deduped by requestId, bounded
    per session (oldest pruned first) and globally across session buckets.
    list() returns structural clones. Nothing is persisted, logged, or written
    into the transcript — state dies with the process, matching the runtime's own
    pending-request lifetime.
  • agent/askTool/pending operation + pi_asktool_pending core tool,
    declared exactly like the existing plans/pending spec (read, input
    argument sessionId optional filter).
  • Ingest points: the local sidecar event path, the native agent event path,
    and the remote event bridge.
  • Cleanup: resolve, abort, session delete, agent_end, tool_end,
    session.archived, sidecar crash, and remote-host connection close.

Why the cleanup paths matter

A disconnected remote host can never resolve its asks, and no later event will
arrive to settle them — input.resolved and session.archived both ride the
same dead stream. Without clearing on close() the dead entries stay visible in
unfiltered listings and their buckets are never reclaimed, so both bounds are
enforced: per session, and across session buckets.

Affected contracts

  • packages/shared/src/protocol.ts: new IPC.invoke.askToolPending channel.
  • MCP control catalog: one new read operation (agent/askTool/pending).
    No existing operation, channel, or payload shape changes.
  • No changes to the frozen process model, persistence ownership, or permission
    boundaries.

Validation (from the request worktree, on top of latest origin/main)

Command Result
pnpm check:pr-base PASS — origin/main (03e03d4cc) is an ancestor of HEAD
pnpm build:js PASS
node --test apps/desktop/test/{mcp-control,pending-asks,remote-event-bridge,remote-host-connection}.test.mjs 36/36 PASS
pnpm --filter @pi-desktop/desktop typecheck PASS
pnpm --filter @pi-desktop/shared typecheck PASS
pnpm lint:biome PASS (86 files, no fixes)

Not run / remaining risk

  • E2E suites that drive a live Electron app were not run (test:e2e:*). They
    need a full desktop session; the coverage above is unit + type + build level.
  • pnpm --filter @pi-desktop/desktop test also fails in this tree on
    apps/desktop/test/settings-remote-hosts.test.mjs — a pre-existing assertion
    that this change does not touch (it expects an aria-controls attribute the
    current page does not emit). Reported here so the failing suite is not
    mistaken for a regression from this PR.
  • The registry is in-memory by design: a desktop restart loses pending asks.
    That matches the runtime's own pending-request lifetime, and a client that
    needs the truth re-reads it after reconnect.

Compatibility / migration

None required. Additive read operation and additive IPC channel; the registry is
empty until an asktool_request arrives, and every pre-existing path behaves
byte-for-byte as before.

@muzimu217 muzimu217 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

实现质量很扎实——registry 的上限与淘汰(per-session 20 oldest-first、全局 1024)、list() 返回结构化克隆防突变、remote 前缀清理(remote:<hostKey>: 且带 local:s1 前缀混淆负例)、abort/会话删除/sidecar 崩溃/断连四个生命周期都收口了,145 行测试覆盖也到位。四点意见,前三点会实质影响合并:

1.(仓库惯例缺口)零 spec/E2E 登记。 本 PR 新增了控制面操作 agent/askTool/pending、核心工具 pi_asktool_pending、IPC 通道 askToolPending,但 docs/spec/ 一个文件都没动。对照同域先例 #945:只加一个设置项就同步了 5 份 spec + ADR 0203 + 213 行 E2E driver。按 AGENTS 约定(行为变更必须 spec + E2E 场景登记),至少需要:控制面操作清单所在的 spec、01-ipc-protocol.md 的通道登记、e2e-plan 的场景条目(暂跑不了就按惯例写 E2E: NOT RUN 块并给替代验证)。

2. CI 尚未触发(checks 为空)——如果是首次贡献需要维护者批一次 workflow run;跑起来之前各类门禁都还没验证过。

3.(行为变化确认)toAskToolRequest 的 toolCallId 兜底从 input.parentToolCallId ?? "" 改为 ?? input.id。 注释说 RACP 响应按 input.id 键控,方向应该是对的;但该字段同时被渲染层消费(答案挂接/工具行匹配),顶层 ask 以前拿到的是 ""——建议确认渲染层没有依赖旧空值的匹配分支;若有渲染层快照/匹配测试最好补一条。

4.(隐私面确认)list() 不带 sessionId 时跨所有会话(含 remote)返回问题原文。 控制面是本机 127.0.0.1 信任边界,但问题文本可能带用户上下文——建议要么默认按会话过滤、要么在 spec 里把"跨会话列举"写成显式语义,二选一即可。

JoyaWang added a commit to JoyaWang/PI-Desktop that referenced this pull request Sep 26, 2026
…e review

Register what vastsa#1014 added and close the four review points:

- 01-ipc-protocol.md (en + zh-CN): list the
  pi-desktop/agent/askTool/pending channel and pi_asktool_pending in
  the 13d tool list, and document the cross-session listing semantics
  (mirroring plans/pending) plus the registry's process-memory lifetime.
- 04-e2e-test-plan.md (en + zh-CN): add
  E2E-MCP-pending-asktool-questions-are-readable after E2E-220 with a
  NOT RUN record (reason, alternative validation, remaining risk) and
  extend the traceability matrix row.
- permission-inline.test.mjs: pin that a renderer ask with a non-empty
  toolCallId is cleared by its own tool_end, guarding the
  parentToolCallId ?? input.id fallback the review flagged.
- native-pi-sessions / session-collaboration-ipc harnesses: declare the
  new ../pending-asks dependency the strict IPC whitelist load rejects.

Validation: docs:check (81 pairs, 516 pages), the seven touched test
files 80/80, lint:biome, typecheck desktop+shared, check:pr-base.
Full desktop suite: 2922 pass, 3 pre-existing environment failures
(browser-cdp, bundled-plugins, plugin-work-panel-views) that also fail
on a clean upstream/main checkout.
@JoyaWang

Copy link
Copy Markdown
Contributor Author

Thank you for the review — all four points are addressed in aa23caf43, which also rebases the branch onto the current main (ccf66728c); the rebase was conflict-free.

1. Spec / E2E registration (done)

  • docs/spec/03-runtime/01-ipc-protocol.md §13d (and its zh-CN mirror): the pi-desktop/agent/askTool/pending channel joins the channel examples, pi_asktool_pending joins the named-tool list, and a paragraph documents the operation: optional sessionId filter, cross-session listing without it, the remote:<hostKey>:<hostSessionId> bucket naming, and the registry's process-memory lifetime.
  • docs/spec/06-delivery/04-e2e-test-plan.md (and its zh-CN mirror): new scenario E2E-MCP-pending-asktool-questions-are-readable right after E2E-220, plus the §8 traceability-matrix row. Its Status is recorded as NOT RUN for the live-Electron journey with the reason (no-local-E2E policy), the alternative validation (the three unit suites), and the remaining risk (HTTP round trip + live asktool prompt path), per the convention in the same document.
  • We deliberately did not add a new ADR: AGENTS.md §13 requires one only "when appropriate", this is an additive read operation over the already-accepted D370/ADR 0203 plane, and the reviewer's minimum list did not include one. Happy to add one if maintainers prefer.

2. CI (requested)
Both CI and PR base runs landed in action_required on the fork PR — could a maintainer approve one workflow run? The branch now contains latest main, so check:pr-base passes locally.

3. toolCallId fallback (verified safe, pinned by test)
We audited every consumer. The renderer never branches on AskToolRequest.toolCallId for answer attachment — AskToolCard resolves by requestId/sessionId, and renderer dedupe is by requestId. The only consumer is exact-match queue removal on tool_end (src/lib/pending-asks.ts removeAskForToolCall via events-slice.ts). Crucially, the main-process registry rejects an empty toolCallId (pending-asks.ts isNonEmptyString guard), so the old ?? "" behaviour made top-level remote asks un-ingestable there — ?? input.id is what makes them registerable, and the local runtime path already always carries a non-empty id (asserted in packages/agent-runtime/src/runtime.test.ts). We added a renderer-side test in apps/desktop/test/permission-inline.test.mjs pinning that an ask with a non-empty id is cleared by its own tool_end and that unrelated ids remove nothing, so a future fallback regression cannot silently reintroduce the leak.

4. Cross-session list() (kept, semantics now explicit)
We kept unfiltered cross-session listing and wrote it into the spec, because it is the operation's core use case — a client that does not yet know which session is asking must still be able to discover the question, and this matches the sibling plans/pending read, which already returns cross-session rows when sessionId is omitted (host-core pending_for_session: ?1 IS NULL OR session_id = ?1). The trust boundary is unchanged (same loopback bearer-token plane, read risk tag, secret-stripping and catalog exclusion still apply); nothing is persisted, logged, or written to the transcript, and remote buckets disappear when the host connection closes.

Validation on aa23caf43 (rebased on ccf66728c):

  • pnpm check:pr-base PASS (origin/main is now an ancestor)
  • pnpm build:js PASS
  • The four PR suites + the three touched harnesses: 80/80 PASS
  • Full apps/desktop unit suite: 2922 pass / 3 fail — the 3 (browser-cdp, bundled-plugins, plugin-work-panel-views) also fail on a clean upstream/main checkout (verified in a throwaway worktree), so they are environment/pre-existing, not regressions. The earlier settings-remote-hosts and native-pi-sessions/session-collaboration-ipc failures from the old base are gone; the latter two had started failing after the rebase because upstream added a strict IPC-dependency whitelist, which now declares ../pending-asks.
  • pnpm docs:check PASS (81 en/zh pairs, 516 pages), lint:biome PASS, desktop+shared typecheck PASS
  • E2E suites that drive a live Electron app remain NOT RUN (recorded in the scenario)

@vastsa

vastsa commented Sep 26, 2026

Copy link
Copy Markdown
Owner

当前 PR 与最新 main 存在合并冲突,麻烦先同步最新 main 并解决冲突后推送更新。我们已在本地基于最新 main 的合并候选上完成验证:完整 desktop 单测 2974/2974 通过,相关测试 80/80 通过,build、typecheck、lint、docs check 均通过。分支更新后我们会再确认并合入。

The desktop renderer receives every `asktool_request` as an in-memory
`AgentEventEnvelope`, so the requestId lives only in the renderer's zustand
store. The MCP control plane can already answer such a question
(`agent/askTool/resolve`) but had no way to read it back, which left remote
clients unable to discover what an Agent is waiting for: `plans/pending` has a
read operation, `agent/askTool/pending` did not.

Add a process-memory registry of pending asks in Electron main, fed by the same
envelopes the renderer sees, and expose it through a new read operation and
core tool:

- `pending-asks.ts`: registry keyed by session, deduped by requestId, bounded
  per session (oldest pruned first) and globally across session buckets.
  Structural clones on read; process memory only, never persisted or logged.
- `agent/askTool/pending` operation + `pi_asktool_pending` tool, mirroring the
  existing `plans/pending` spec shape.
- Ingested from the local sidecar, the native agent event path and the remote
  event bridge; cleared on resolve, abort, session delete, agent_end, tool_end,
  session archive, and when a remote host connection closes (a disconnected
  host can never resolve its asks, and no later event will arrive to settle
  them).

Validation (from this request worktree, on top of latest origin/main):

- `pnpm check:pr-base` PASS
- `pnpm build:js` PASS
- `node --test apps/desktop/test/{mcp-control,pending-asks,remote-event-bridge,remote-host-connection}.test.mjs`
  36/36 PASS
- `pnpm --filter @pi-desktop/desktop typecheck` PASS
- `pnpm --filter @pi-desktop/shared typecheck` PASS
- `pnpm lint:biome` PASS

Not run: the E2E suites that drive a live Electron app. The full
`pnpm --filter @pi-desktop/desktop test` run also fails on
`apps/desktop/test/settings-remote-hosts.test.mjs`, an untouched pre-existing
assertion in this tree; that file is unrelated to this change.
…e review

Register what vastsa#1014 added and close the four review points:

- 01-ipc-protocol.md (en + zh-CN): list the
  pi-desktop/agent/askTool/pending channel and pi_asktool_pending in
  the 13d tool list, and document the cross-session listing semantics
  (mirroring plans/pending) plus the registry's process-memory lifetime.
- 04-e2e-test-plan.md (en + zh-CN): add
  E2E-MCP-pending-asktool-questions-are-readable after E2E-220 with a
  NOT RUN record (reason, alternative validation, remaining risk) and
  extend the traceability matrix row.
- permission-inline.test.mjs: pin that a renderer ask with a non-empty
  toolCallId is cleared by its own tool_end, guarding the
  parentToolCallId ?? input.id fallback the review flagged.
- native-pi-sessions / session-collaboration-ipc harnesses: declare the
  new ../pending-asks dependency the strict IPC whitelist load rejects.

Validation: docs:check (81 pairs, 516 pages), the seven touched test
files 80/80, lint:biome, typecheck desktop+shared, check:pr-base.
Full desktop suite: 2922 pass, 3 pre-existing environment failures
(browser-cdp, bundled-plugins, plugin-work-panel-views) that also fail
on a clean upstream/main checkout.
@JoyaWang

Copy link
Copy Markdown
Contributor Author

Rebased onto the latest main (78caf9779) and pushed as 67ead6121.

The only conflict was the import block in apps/desktop/electron/main/runtime/sidecar.ts — the branch now carries both the skill-document import from main and the pending-asks import from this PR. Nothing else needed resolution.

Re-validation on 67ead6121:

  • pnpm check:pr-base PASS (78caf9779 is an ancestor)
  • pnpm build:js PASS
  • The eight suites touching this change (mcp-control, pending-asks, remote-event-bridge, remote-host-connection, native-pi-sessions, session-collaboration-ipc, permission-inline, rpc-lifecycle-contract): 94/94 PASS
  • Full node --test apps/desktop/test/*.test.mjs: 2955 pass / 3 fail — browser-cdp, bundled-plugins, plugin-work-panel-views, the same environment failures we verified on a clean upstream/main checkout before
  • typecheck (shared + desktop), lint:biome, pnpm docs:check (81 pairs / 517 pages): PASS

The branch also picked up #1080's crash classification from main — the settleCrashedSession on this branch's crash-cleanup path now uses the honest codes, and our pendingAsksRegistry.clearSession calls ride along unchanged.

Ready for your re-check; CI approval for a workflow run is still pending on this fork PR whenever you get to it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants