✨ feat(hetero-agent): support AskUserQuestion tools for claude code#14639
Merged
Conversation
…OBE-8725 step 1+2)
Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's
built-in tool short-circuits in `-p` mode, so we host an in-process MCP
server that exposes an equivalent `ask_user_question` tool. The handler
blocks until the consumer submits an answer (or the 5min deadline / op
shutdown fires), surfacing a structured `agent_intervention_request` /
`agent_intervention_response` round-trip on the existing event stream.
Added in this commit:
- `packages/heterogeneous-agents/src/askUser/`
- `AskUserBridge` — per-op pending map with timeout / cancel / progress
keepalive support; emits an async-iterable of outbound events
- `AskUserMcpServer` — process-wide HTTP/Streamable MCP server,
`?op=<id>` query routes via `AsyncLocalStorage` →
`onsessioninitialized` → sessionId↔opId map; tool handler hands off
to the matching bridge and pumps `notifications/progress` back to CC
every 30s as wire-level keepalive (required for >5min waits, see
spike notes)
- `constants.ts` — shared tool/server names + the stable `apiName`
the adapter rewrites to
- Unit tests cover bridge lifecycle (resolve / cancel / timeout /
progress / event stream) and an end-to-end MCP probe via
`StreamableHTTPClientTransport`
- `packages/agent-gateway-client/src/types.ts` — wire-level
`agent_intervention_request` / `agent_intervention_response` event
variants + payload interfaces. Re-exported through the package barrel.
- `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's
`tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter
rewrites `apiName` to `askUserQuestion` so the renderer routes on a
clean domain key. Identifier stays `claude-code`. Applied to both the
main-agent and subagent paths for symmetry (subagent ask isn't
expected today, but doesn't hurt).
- `src/server/routers/lambda/aiAgent.ts` — Zod input schema for
`aiAgent.heteroIngest` extended with the two new event types so the
CLI sandbox can forward them through the server.
No producer wiring yet — Steps 3-5 plug this into Electron main, the
renderer executor, and the new UI.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## canary #14639 +/- ##
==========================================
- Coverage 65.98% 65.93% -0.06%
==========================================
Files 2891 2891
Lines 250534 250750 +216
Branches 25155 29291 +4136
==========================================
+ Hits 165317 165333 +16
- Misses 85065 85265 +200
Partials 152 152
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
…r (LOBE-8725 step 3)
Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the
desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now
goes live during real prompts; renderer-submitted answers route back via
new IPC.
Changes
- `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add
optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so
controller-managed temp configs flow into the driver.
- `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts`
— append `--mcp-config <path>` when provided. Disallowed-tools pin
stays so CC's built-in AskUserQuestion remains off (avoids double-
registration of the same tool name).
- `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts`
- Lazy-singleton `AskUserMcpServer` started on first claude-code prompt
(de-duped concurrent first-callers via in-flight promise).
- Per-op `setupInterventionForOp(opId, sessionId)`: registers an
`AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with
`alwaysLoad: true` so CC eager-loads the tool (1-hop call, no
ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()`
into the existing `heteroAgentEvent` broadcast.
- Cleanup paths: exit handler `await intervention.cleanup()` settles
pending MCP handlers + unlinks the temp config; pre-spawn errors
short-circuit the same cleanup so we don't leak bridges on
`buildSpawnPlan` / trace-session failures.
- `before-quit` stops the MCP server (in addition to killing CC
processes).
- New `@IpcMethod() submitIntervention({ operationId, toolCallId,
result?, cancelled?, cancelReason? })` — renderer side will dispatch
answers / cancellations through this in Step 4/5.
- codex unchanged — bridge setup is gated on `agentType === 'claude-code'`.
- `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy
for `submitIntervention`.
- New `claudeCode.test.ts` covers the four driver-arg paths
(`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay
disallowed). Existing 28 controller tests still pass.
What still doesn't run end-to-end
- The renderer `heteroExecutor` doesn't consume `agent_intervention_request`
yet — events go through the broadcast but the chat store ignores them.
- No UI to render the intervention card or to call `submitIntervention`.
Both lands in Steps 4/5 next.
…erer handler (LOBE-8725 step 3.5+4) Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId` from MCP `_meta`) instead of a random UUID, so the `agent_intervention_request` event references the same id as the existing tool message on the renderer side. Renderer-side `heteroExecutor` learns the new event: - Added `persistInterventionRequest(...)` next to `persistToolResult` — stamps `pluginState.askUserQuestion` (apiName + identifier + questions parsed from `arguments` + deadline + status='pending' + toolCallId) onto the matching tool message via `messageService.updateToolMessage`. - New branch in `handleStreamEvent` for `'agent_intervention_request'`: defers behind `persistQueue` (so it lands AFTER `persistToolBatch` populates `toolMsgIdByCallId`), then mirrors the same pluginState onto the in-memory message via `internal_dispatchMessage` so the UI lights up immediately — no fetchAndReplaceMessages round-trip needed. - The eventual `tool_result` for the same toolCallId hits the existing `tool_result` branch unchanged: it overwrites `pluginState` with whatever the result carries (typically undefined for our MCP tool, so `pluginState.askUserQuestion` clears and the intervention UI yields to the regular Render). Bridge tests cover the new contract: - caller-supplied toolCallId becomes the wire correlation key - duplicate-toolCallId pendings reject loudly so two-handler clobbers surface immediately 153 package tests + 1167 desktop main tests + 51 hetero executor tests still green; type-check clean.
…OBE-8725 step 5)
Dedicated Render for the synthetic `askUserQuestion` apiName the adapter
rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives
under CC's render registry so the existing chat tool-detail flow picks
it up automatically — no changes to the conversation framework.
- New `AskUserQuestionItem` / `AskUserQuestionArgs` /
`AskUserQuestionPluginState` types (mirrors CC's own
AskUserQuestion schema verbatim).
- `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'`
member so the renders / inspectors / streamings registries can key
off the same enum value.
- `client/Render/AskUserQuestion/index.tsx` is the component:
- `pluginState.askUserQuestion?.status === 'pending'` → renders the
questions form (Select for single-select, CheckboxGroup for
multi-select), a 5-min countdown ticking once a second, Submit /
Skip buttons. Reads `operationId` via `messageOperationMap` so we
can route through `heterogeneousAgentService.submitIntervention`.
- Otherwise → renders the questions as muted captions plus the
final answer text from `content`. Surfaces a warning when the
tool_result was an error (timeout / cancelled / session ended).
- Submit button stays disabled until every question has a
selection; Skip always enabled (sends `cancelled: true`).
- `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers
the new component.
What this does NOT do
- Doesn't touch `BuiltinToolInterventions` — the form is rendered
inside the regular tool body (Render slot), not the canonical
intervention slot. Cleanest for now: the framework intervention
flow assumes `submitToolInteraction` store actions, which would
fight our IPC path. We can refactor onto that surface later if
CC grows additional interactions (approval, file picker).
- Doesn't translate strings — i18n in a follow-up.
Type-check clean. Step 6 (real desktop e2e via CC) is next.
… (LOBE-8725 step 5 follow-up) Step 5 registered the Render component but stopped at the registry — the chat tool-detail still returned the loading placeholder while `isToolCalling` was true, so users only ever saw a spinner during the 5 min intervention window. Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on CC + apiName=askUserQuestion tool messages) and route to the registered builtin Render inline before the placeholder branch. Once the intervention resolves, the eventual `tool_result` clears `pluginState.askUserQuestion` and the regular Render takes over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…LOBE-8519 follow-up) LOBE-8519 left two TODOs in `generationSlice` where hetero runtime silently fell through to client mode — regenerate would secretly hit the agent's underlying LLM, and continue would synthesize a fake "please continue" turn that confuses CC / Codex. - regenerateMessage: re-create the assistant row branched off the same user message, resolve resume sessionId (drop on cwd mismatch), then spawn a child `execHeterogeneousAgent` op so Stop only kills the executor, not the parent regenerate op. Mirrors sendMessage's hetero branch. - continueGenerationMessage: hetero CLIs have no continue primitive — each prompt is a fresh user turn — so bail out instead of polluting the session. - continueGenerationMessage: gateway mode now branches a server-side resume run instead of falling through to client. Surfaced while testing CC AskUserQuestion end-to-end on the LOBE-8725 branch (regenerating after an answered question went through the wrong runtime). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs surfaced when invoking the local-testing helper from a fresh session on macOS: - `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit code propagates through `pipefail`. With `set -e`, an empty pid set silently kills the whole script — `do_start` reported success, no Electron, no error. Trail with `|| true`. - `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`; process-tree teardown still works because `expand_descendants` walks the tree directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…BE-8725) `AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across every CC subprocess. The SDK transport latches `_initialized=true` after the first `initialize`, so the second op's CC subprocess sees `Invalid Request: Server already initialized` (400) and reports the `lobe_cc` server as `failed`. From the model's POV the MCP tool is absent — it falls back to ToolSearch, can't find anything, and verbalizes the question instead. Refactor to the canonical multi-tenant pattern: one transport + one `McpServer` per session, looked up by the SDK-managed `mcp-session-id` header. New transports are minted on the first POST without a session id (must be an `initialize` request); subsequent requests route via the stored map; `onsessionclosed` cleans up. The first run of any process still works as before — this only matters once a second op spins up. Added a 3-op sequential regression test that fails on the old single-transport implementation and passes now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ntion surface (LOBE-8725)
Step 5's first cut shoehorned the pending form into the Render slot and
drove submit/skip with a custom `pluginState.askUserQuestion.status`
field, which forced three layers of glue:
- `Tool/Detail` had to bypass the loading placeholder via an
identifier+apiName hardcode so the form would surface during
`isToolCalling`
- The executor had to `messageService.getMessages → replaceMessages`
after `agent_intervention_request` to drag the freshly-created tool
row into in-memory state (the framework's own `tool_end →
fetchAndReplaceMessages` only fires after the user answers)
- The executor also had to `associateMessageWithOperation` for the tool
row so the form could look up the running CC op for IPC
All three were patches around skipping the canonical surface. This
commit moves AskUserQuestion onto `pluginIntervention.status='pending'`
and the `BuiltinToolInterventions` registry, which the framework
already drives end-to-end:
- `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx`
— pure form, no IPC, no store reads. Resolves through the standard
`onInteractionAction({type:'submit'|'skip'|'cancel'})` callback.
- `Render/AskUserQuestion` shrinks to the answered/aborted view only;
the framework hides Render while pending, so no status switching.
- New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}"
chip in the inline tool body, matching the rest of CC's tools.
- Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new
`ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`;
`BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry.
Hetero needs a different action handler than `submitToolInteraction`
(which spawns `executeClientAgent` — wrong for a CC subprocess that's
already blocked on an MCP call). Two thin pieces wire that:
- `submitHeteroIntervention` (chat store) — sets
`pluginIntervention` via `optimisticUpdateMessagePlugin` (which
already syncs DB + in-memory + parent-assistant `tools[].intervention`
in one shot), then forwards the answer through
`heterogeneousAgentService.submitIntervention` IPC. Operation lookup
walks the tool message's `parentId` to hit the assistant's
`messageOperationMap` entry — drops the explicit
`associateMessageWithOperation` call from the executor.
- `customInteractionHandlers.isHeteroInteractionIdentifier` flags
`ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits
there before reaching the existing `submitToolInteraction` path.
Executor change collapses to one line:
`optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`.
The post-intervention refresh, the associate call, and the
`persistInterventionRequest` helper all go away.
Removed:
- `AskUserQuestionPluginState` type (custom field is gone)
- `Tool/Detail` `askUserPending` inline-render branch
- Executor `messageService.getMessages + replaceMessages` round-trip
- Executor `associateMessageWithOperation` for tool rows
- `persistInterventionRequest` helper
Verified end-to-end against a real CC subprocess on desktop:
- Inline body shows the new Inspector chip; pending form lives in the
bottom InterventionBar (canonical surface)
- Submit ships answer through MCP, CC continues with structured result
- Skip flips status to `rejected`, framework's RejectedResponse
shows "User skipped"; CC receives isError and falls back to text
- `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op
(the per-session transport fix from the previous commit)
- `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n intervention (LOBE-8725)
Select dropdown was the wrong primitive — it hides options behind an extra
click and doesn't read like a question to answer. CC's underlying tool is
1-4 questions × 2-4 options, so the whole option set always fits inline.
- Each option renders as a clickable card: numbered chip (1/2/3/4) +
bold label + secondary description on a single row. Hover tints the
background; selected state lights up `colorPrimary` on both the chip
and the card outline so the pick is unmistakable at a glance.
- Multi-select (`q.multiSelect`) toggles instead of replacing, with a
"(multi-select)" hint in the question header.
- Multi-question support gets a proper visual hierarchy: each question
past the first sits below a dashed divider, headed by a `Q1/N` tag
+ the original `q.header` chip. The `Q*/N` lets the user track
progress without counting.
- Inspector picks up the question count too: now shows
"askUserQuestion · {first header} +N" when multiple are queued.
Verified end-to-end on desktop with a CC-driven 2-question prompt
(4-option + 3-option). Both selections feed back to CC as a single
"User answers" payload, CC echoes both picks in its continuation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… for AskUserQuestion (LOBE-8725) - Multi-question forms now use a top tab strip; single question renders inline. - Picking a single-select option auto-advances to the next unanswered question. - Drafts persist to tool message `pluginState.askUserDraft` so picks survive remount / HMR; new `setInterventionDraft` action on the chat store dispatches the pluginState patch. - Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for every unanswered question instead of letting the bridge time out into a cancelled isError — model gets a structured answer it can act on. - Visual: selected option now uses filled `colorPrimaryBg` + right-aligned check icon; index chip stays neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…LOBE-8725) The async exit-handler cleanup raced Electron's main-process teardown and left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync unlink in the quit hook is the only reliable guarantee. Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q or `app.quit()`, not on external kills (test harness, OS shutdown). Verified by manual test: pending askUserQuestion forms now leave zero residue after both Cmd+Q and SIGTERM paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… render (LOBE-8725)
Submit now writes the structured `{ questionText: pickedLabel(s) }` payload
to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so
Render no longer has to scrape the bridge's prose `User answers:` content.
Render shows one Q&A block per question — header + question + a checkmark
card per picked option (multi-select fans out into multiple rows). Falls
back to a `—` placeholder when answers are missing (older messages or
skipped flows), and keeps the existing `pluginError` warning for cancel /
no-answer paths.
Also surfaces the answers in the Skill state inspector tab, which was
previously empty for completed askUserQuestion messages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…on temp configs (LOBE-8725) Locks down the regression fixed in c0de0cd — async exit-handler cleanup losing to Electron's main-process teardown. Four cases: `before-quit` (Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown), `SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not throw on the second pass). `process.on` and `process.exit` are stubbed in the signal-path tests so the controller's listener attaches to a spy, not the test runner's process — otherwise we'd leak a real SIGTERM listener every test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
emaxlele
pushed a commit
to emaxlele/lobehub
that referenced
this pull request
May 10, 2026
…obehub#14639) * ✨ feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2) Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's built-in tool short-circuits in `-p` mode, so we host an in-process MCP server that exposes an equivalent `ask_user_question` tool. The handler blocks until the consumer submits an answer (or the 5min deadline / op shutdown fires), surfacing a structured `agent_intervention_request` / `agent_intervention_response` round-trip on the existing event stream. Added in this commit: - `packages/heterogeneous-agents/src/askUser/` - `AskUserBridge` — per-op pending map with timeout / cancel / progress keepalive support; emits an async-iterable of outbound events - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server, `?op=<id>` query routes via `AsyncLocalStorage` → `onsessioninitialized` → sessionId↔opId map; tool handler hands off to the matching bridge and pumps `notifications/progress` back to CC every 30s as wire-level keepalive (required for >5min waits, see spike notes) - `constants.ts` — shared tool/server names + the stable `apiName` the adapter rewrites to - Unit tests cover bridge lifecycle (resolve / cancel / timeout / progress / event stream) and an end-to-end MCP probe via `StreamableHTTPClientTransport` - `packages/agent-gateway-client/src/types.ts` — wire-level `agent_intervention_request` / `agent_intervention_response` event variants + payload interfaces. Re-exported through the package barrel. - `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter rewrites `apiName` to `askUserQuestion` so the renderer routes on a clean domain key. Identifier stays `claude-code`. Applied to both the main-agent and subagent paths for symmetry (subagent ask isn't expected today, but doesn't hurt). - `src/server/routers/lambda/aiAgent.ts` — Zod input schema for `aiAgent.heteroIngest` extended with the two new event types so the CLI sandbox can forward them through the server. No producer wiring yet — Steps 3-5 plug this into Electron main, the renderer executor, and the new UI. * ✨ feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3) Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now goes live during real prompts; renderer-submitted answers route back via new IPC. Changes - `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so controller-managed temp configs flow into the driver. - `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts` — append `--mcp-config <path>` when provided. Disallowed-tools pin stays so CC's built-in AskUserQuestion remains off (avoids double- registration of the same tool name). - `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts` - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt (de-duped concurrent first-callers via in-flight promise). - Per-op `setupInterventionForOp(opId, sessionId)`: registers an `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()` into the existing `heteroAgentEvent` broadcast. - Cleanup paths: exit handler `await intervention.cleanup()` settles pending MCP handlers + unlinks the temp config; pre-spawn errors short-circuit the same cleanup so we don't leak bridges on `buildSpawnPlan` / trace-session failures. - `before-quit` stops the MCP server (in addition to killing CC processes). - New `@IpcMethod() submitIntervention({ operationId, toolCallId, result?, cancelled?, cancelReason? })` — renderer side will dispatch answers / cancellations through this in Step 4/5. - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`. - `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy for `submitIntervention`. - New `claudeCode.test.ts` covers the four driver-arg paths (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay disallowed). Existing 28 controller tests still pass. What still doesn't run end-to-end - The renderer `heteroExecutor` doesn't consume `agent_intervention_request` yet — events go through the broadcast but the chat store ignores them. - No UI to render the intervention card or to call `submitIntervention`. Both lands in Steps 4/5 next. * ✨ feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4) Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId` from MCP `_meta`) instead of a random UUID, so the `agent_intervention_request` event references the same id as the existing tool message on the renderer side. Renderer-side `heteroExecutor` learns the new event: - Added `persistInterventionRequest(...)` next to `persistToolResult` — stamps `pluginState.askUserQuestion` (apiName + identifier + questions parsed from `arguments` + deadline + status='pending' + toolCallId) onto the matching tool message via `messageService.updateToolMessage`. - New branch in `handleStreamEvent` for `'agent_intervention_request'`: defers behind `persistQueue` (so it lands AFTER `persistToolBatch` populates `toolMsgIdByCallId`), then mirrors the same pluginState onto the in-memory message via `internal_dispatchMessage` so the UI lights up immediately — no fetchAndReplaceMessages round-trip needed. - The eventual `tool_result` for the same toolCallId hits the existing `tool_result` branch unchanged: it overwrites `pluginState` with whatever the result carries (typically undefined for our MCP tool, so `pluginState.askUserQuestion` clears and the intervention UI yields to the regular Render). Bridge tests cover the new contract: - caller-supplied toolCallId becomes the wire correlation key - duplicate-toolCallId pendings reject loudly so two-handler clobbers surface immediately 153 package tests + 1167 desktop main tests + 51 hetero executor tests still green; type-check clean. * ✨ feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5) Dedicated Render for the synthetic `askUserQuestion` apiName the adapter rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives under CC's render registry so the existing chat tool-detail flow picks it up automatically — no changes to the conversation framework. - New `AskUserQuestionItem` / `AskUserQuestionArgs` / `AskUserQuestionPluginState` types (mirrors CC's own AskUserQuestion schema verbatim). - `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'` member so the renders / inspectors / streamings registries can key off the same enum value. - `client/Render/AskUserQuestion/index.tsx` is the component: - `pluginState.askUserQuestion?.status === 'pending'` → renders the questions form (Select for single-select, CheckboxGroup for multi-select), a 5-min countdown ticking once a second, Submit / Skip buttons. Reads `operationId` via `messageOperationMap` so we can route through `heterogeneousAgentService.submitIntervention`. - Otherwise → renders the questions as muted captions plus the final answer text from `content`. Surfaces a warning when the tool_result was an error (timeout / cancelled / session ended). - Submit button stays disabled until every question has a selection; Skip always enabled (sends `cancelled: true`). - `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers the new component. What this does NOT do - Doesn't touch `BuiltinToolInterventions` — the form is rendered inside the regular tool body (Render slot), not the canonical intervention slot. Cleanest for now: the framework intervention flow assumes `submitToolInteraction` store actions, which would fight our IPC path. We can refactor onto that surface later if CC grows additional interactions (approval, file picker). - Doesn't translate strings — i18n in a follow-up. Type-check clean. Step 6 (real desktop e2e via CC) is next. * ✨ feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up) Step 5 registered the Render component but stopped at the registry — the chat tool-detail still returned the loading placeholder while `isToolCalling` was true, so users only ever saw a spinner during the 5 min intervention window. Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on CC + apiName=askUserQuestion tool messages) and route to the registered builtin Render inline before the placeholder branch. Once the intervention resolves, the eventual `tool_result` clears `pluginState.askUserQuestion` and the regular Render takes over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up) LOBE-8519 left two TODOs in `generationSlice` where hetero runtime silently fell through to client mode — regenerate would secretly hit the agent's underlying LLM, and continue would synthesize a fake "please continue" turn that confuses CC / Codex. - regenerateMessage: re-create the assistant row branched off the same user message, resolve resume sessionId (drop on cwd mismatch), then spawn a child `execHeterogeneousAgent` op so Stop only kills the executor, not the parent regenerate op. Mirrors sendMessage's hetero branch. - continueGenerationMessage: hetero CLIs have no continue primitive — each prompt is a fresh user turn — so bail out instead of polluting the session. - continueGenerationMessage: gateway mode now branches a server-side resume run instead of falling through to client. Surfaced while testing CC AskUserQuestion end-to-end on the LOBE-8725 branch (regenerating after an answered question went through the wrong runtime). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2 Two bugs surfaced when invoking the local-testing helper from a fresh session on macOS: - `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit code propagates through `pipefail`. With `set -e`, an empty pid set silently kills the whole script — `do_start` reported success, no Electron, no error. Trail with `|| true`. - `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`; process-tree teardown still works because `expand_descendants` walks the tree directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725) `AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across every CC subprocess. The SDK transport latches `_initialized=true` after the first `initialize`, so the second op's CC subprocess sees `Invalid Request: Server already initialized` (400) and reports the `lobe_cc` server as `failed`. From the model's POV the MCP tool is absent — it falls back to ToolSearch, can't find anything, and verbalizes the question instead. Refactor to the canonical multi-tenant pattern: one transport + one `McpServer` per session, looked up by the SDK-managed `mcp-session-id` header. New transports are minted on the first POST without a session id (must be an `initialize` request); subsequent requests route via the stored map; `onsessionclosed` cleans up. The first run of any process still works as before — this only matters once a second op spins up. Added a 3-op sequential regression test that fails on the old single-transport implementation and passes now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725) Step 5's first cut shoehorned the pending form into the Render slot and drove submit/skip with a custom `pluginState.askUserQuestion.status` field, which forced three layers of glue: - `Tool/Detail` had to bypass the loading placeholder via an identifier+apiName hardcode so the form would surface during `isToolCalling` - The executor had to `messageService.getMessages → replaceMessages` after `agent_intervention_request` to drag the freshly-created tool row into in-memory state (the framework's own `tool_end → fetchAndReplaceMessages` only fires after the user answers) - The executor also had to `associateMessageWithOperation` for the tool row so the form could look up the running CC op for IPC All three were patches around skipping the canonical surface. This commit moves AskUserQuestion onto `pluginIntervention.status='pending'` and the `BuiltinToolInterventions` registry, which the framework already drives end-to-end: - `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx` — pure form, no IPC, no store reads. Resolves through the standard `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback. - `Render/AskUserQuestion` shrinks to the answered/aborted view only; the framework hides Render while pending, so no status switching. - New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}" chip in the inline tool body, matching the rest of CC's tools. - Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`; `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry. Hetero needs a different action handler than `submitToolInteraction` (which spawns `executeClientAgent` — wrong for a CC subprocess that's already blocked on an MCP call). Two thin pieces wire that: - `submitHeteroIntervention` (chat store) — sets `pluginIntervention` via `optimisticUpdateMessagePlugin` (which already syncs DB + in-memory + parent-assistant `tools[].intervention` in one shot), then forwards the answer through `heterogeneousAgentService.submitIntervention` IPC. Operation lookup walks the tool message's `parentId` to hit the assistant's `messageOperationMap` entry — drops the explicit `associateMessageWithOperation` call from the executor. - `customInteractionHandlers.isHeteroInteractionIdentifier` flags `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits there before reaching the existing `submitToolInteraction` path. Executor change collapses to one line: `optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`. The post-intervention refresh, the associate call, and the `persistInterventionRequest` helper all go away. Removed: - `AskUserQuestionPluginState` type (custom field is gone) - `Tool/Detail` `askUserPending` inline-render branch - Executor `messageService.getMessages + replaceMessages` round-trip - Executor `associateMessageWithOperation` for tool rows - `persistInterventionRequest` helper Verified end-to-end against a real CC subprocess on desktop: - Inline body shows the new Inspector chip; pending form lives in the bottom InterventionBar (canonical surface) - Submit ships answer through MCP, CC continues with structured result - Skip flips status to `rejected`, framework's RejectedResponse shows "User skipped"; CC receives isError and falls back to text - `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op (the per-session transport fix from the previous commit) - `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725) Select dropdown was the wrong primitive — it hides options behind an extra click and doesn't read like a question to answer. CC's underlying tool is 1-4 questions × 2-4 options, so the whole option set always fits inline. - Each option renders as a clickable card: numbered chip (1/2/3/4) + bold label + secondary description on a single row. Hover tints the background; selected state lights up `colorPrimary` on both the chip and the card outline so the pick is unmistakable at a glance. - Multi-select (`q.multiSelect`) toggles instead of replacing, with a "(multi-select)" hint in the question header. - Multi-question support gets a proper visual hierarchy: each question past the first sits below a dashed divider, headed by a `Q1/N` tag + the original `q.header` chip. The `Q*/N` lets the user track progress without counting. - Inspector picks up the question count too: now shows "askUserQuestion · {first header} +N" when multiple are queued. Verified end-to-end on desktop with a CC-driven 2-question prompt (4-option + 3-option). Both selections feed back to CC as a single "User answers" payload, CC echoes both picks in its continuation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725) - Multi-question forms now use a top tab strip; single question renders inline. - Picking a single-select option auto-advances to the next unanswered question. - Drafts persist to tool message `pluginState.askUserDraft` so picks survive remount / HMR; new `setInterventionDraft` action on the chat store dispatches the pluginState patch. - Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for every unanswered question instead of letting the bridge time out into a cancelled isError — model gets a structured answer it can act on. - Visual: selected option now uses filled `colorPrimaryBg` + right-aligned check icon; index chip stays neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725) The async exit-handler cleanup raced Electron's main-process teardown and left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync unlink in the quit hook is the only reliable guarantee. Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q or `app.quit()`, not on external kills (test harness, OS shutdown). Verified by manual test: pending askUserQuestion forms now leave zero residue after both Cmd+Q and SIGTERM paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725) Submit now writes the structured `{ questionText: pickedLabel(s) }` payload to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so Render no longer has to scrape the bridge's prose `User answers:` content. Render shows one Q&A block per question — header + question + a checkmark card per picked option (multi-select fans out into multiple rows). Falls back to a `—` placeholder when answers are missing (older messages or skipped flows), and keeps the existing `pluginError` warning for cancel / no-answer paths. Also surfaces the answers in the Skill state inspector tab, which was previously empty for completed askUserQuestion messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725) Locks down the regression fixed in c0de0cd — async exit-handler cleanup losing to Electron's main-process teardown. Four cases: `before-quit` (Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown), `SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not throw on the second pass). `process.on` and `process.exit` are stubbed in the signal-path tests so the controller's listener attaches to a spy, not the test runner's process — otherwise we'd leak a real SIGTERM listener every test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
emaxlele
pushed a commit
to emaxlele/lobehub
that referenced
this pull request
May 10, 2026
…obehub#14639) * ✨ feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2) Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's built-in tool short-circuits in `-p` mode, so we host an in-process MCP server that exposes an equivalent `ask_user_question` tool. The handler blocks until the consumer submits an answer (or the 5min deadline / op shutdown fires), surfacing a structured `agent_intervention_request` / `agent_intervention_response` round-trip on the existing event stream. Added in this commit: - `packages/heterogeneous-agents/src/askUser/` - `AskUserBridge` — per-op pending map with timeout / cancel / progress keepalive support; emits an async-iterable of outbound events - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server, `?op=<id>` query routes via `AsyncLocalStorage` → `onsessioninitialized` → sessionId↔opId map; tool handler hands off to the matching bridge and pumps `notifications/progress` back to CC every 30s as wire-level keepalive (required for >5min waits, see spike notes) - `constants.ts` — shared tool/server names + the stable `apiName` the adapter rewrites to - Unit tests cover bridge lifecycle (resolve / cancel / timeout / progress / event stream) and an end-to-end MCP probe via `StreamableHTTPClientTransport` - `packages/agent-gateway-client/src/types.ts` — wire-level `agent_intervention_request` / `agent_intervention_response` event variants + payload interfaces. Re-exported through the package barrel. - `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter rewrites `apiName` to `askUserQuestion` so the renderer routes on a clean domain key. Identifier stays `claude-code`. Applied to both the main-agent and subagent paths for symmetry (subagent ask isn't expected today, but doesn't hurt). - `src/server/routers/lambda/aiAgent.ts` — Zod input schema for `aiAgent.heteroIngest` extended with the two new event types so the CLI sandbox can forward them through the server. No producer wiring yet — Steps 3-5 plug this into Electron main, the renderer executor, and the new UI. * ✨ feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3) Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now goes live during real prompts; renderer-submitted answers route back via new IPC. Changes - `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so controller-managed temp configs flow into the driver. - `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts` — append `--mcp-config <path>` when provided. Disallowed-tools pin stays so CC's built-in AskUserQuestion remains off (avoids double- registration of the same tool name). - `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts` - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt (de-duped concurrent first-callers via in-flight promise). - Per-op `setupInterventionForOp(opId, sessionId)`: registers an `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()` into the existing `heteroAgentEvent` broadcast. - Cleanup paths: exit handler `await intervention.cleanup()` settles pending MCP handlers + unlinks the temp config; pre-spawn errors short-circuit the same cleanup so we don't leak bridges on `buildSpawnPlan` / trace-session failures. - `before-quit` stops the MCP server (in addition to killing CC processes). - New `@IpcMethod() submitIntervention({ operationId, toolCallId, result?, cancelled?, cancelReason? })` — renderer side will dispatch answers / cancellations through this in Step 4/5. - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`. - `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy for `submitIntervention`. - New `claudeCode.test.ts` covers the four driver-arg paths (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay disallowed). Existing 28 controller tests still pass. What still doesn't run end-to-end - The renderer `heteroExecutor` doesn't consume `agent_intervention_request` yet — events go through the broadcast but the chat store ignores them. - No UI to render the intervention card or to call `submitIntervention`. Both lands in Steps 4/5 next. * ✨ feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4) Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId` from MCP `_meta`) instead of a random UUID, so the `agent_intervention_request` event references the same id as the existing tool message on the renderer side. Renderer-side `heteroExecutor` learns the new event: - Added `persistInterventionRequest(...)` next to `persistToolResult` — stamps `pluginState.askUserQuestion` (apiName + identifier + questions parsed from `arguments` + deadline + status='pending' + toolCallId) onto the matching tool message via `messageService.updateToolMessage`. - New branch in `handleStreamEvent` for `'agent_intervention_request'`: defers behind `persistQueue` (so it lands AFTER `persistToolBatch` populates `toolMsgIdByCallId`), then mirrors the same pluginState onto the in-memory message via `internal_dispatchMessage` so the UI lights up immediately — no fetchAndReplaceMessages round-trip needed. - The eventual `tool_result` for the same toolCallId hits the existing `tool_result` branch unchanged: it overwrites `pluginState` with whatever the result carries (typically undefined for our MCP tool, so `pluginState.askUserQuestion` clears and the intervention UI yields to the regular Render). Bridge tests cover the new contract: - caller-supplied toolCallId becomes the wire correlation key - duplicate-toolCallId pendings reject loudly so two-handler clobbers surface immediately 153 package tests + 1167 desktop main tests + 51 hetero executor tests still green; type-check clean. * ✨ feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5) Dedicated Render for the synthetic `askUserQuestion` apiName the adapter rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives under CC's render registry so the existing chat tool-detail flow picks it up automatically — no changes to the conversation framework. - New `AskUserQuestionItem` / `AskUserQuestionArgs` / `AskUserQuestionPluginState` types (mirrors CC's own AskUserQuestion schema verbatim). - `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'` member so the renders / inspectors / streamings registries can key off the same enum value. - `client/Render/AskUserQuestion/index.tsx` is the component: - `pluginState.askUserQuestion?.status === 'pending'` → renders the questions form (Select for single-select, CheckboxGroup for multi-select), a 5-min countdown ticking once a second, Submit / Skip buttons. Reads `operationId` via `messageOperationMap` so we can route through `heterogeneousAgentService.submitIntervention`. - Otherwise → renders the questions as muted captions plus the final answer text from `content`. Surfaces a warning when the tool_result was an error (timeout / cancelled / session ended). - Submit button stays disabled until every question has a selection; Skip always enabled (sends `cancelled: true`). - `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers the new component. What this does NOT do - Doesn't touch `BuiltinToolInterventions` — the form is rendered inside the regular tool body (Render slot), not the canonical intervention slot. Cleanest for now: the framework intervention flow assumes `submitToolInteraction` store actions, which would fight our IPC path. We can refactor onto that surface later if CC grows additional interactions (approval, file picker). - Doesn't translate strings — i18n in a follow-up. Type-check clean. Step 6 (real desktop e2e via CC) is next. * ✨ feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up) Step 5 registered the Render component but stopped at the registry — the chat tool-detail still returned the loading placeholder while `isToolCalling` was true, so users only ever saw a spinner during the 5 min intervention window. Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on CC + apiName=askUserQuestion tool messages) and route to the registered builtin Render inline before the placeholder branch. Once the intervention resolves, the eventual `tool_result` clears `pluginState.askUserQuestion` and the regular Render takes over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up) LOBE-8519 left two TODOs in `generationSlice` where hetero runtime silently fell through to client mode — regenerate would secretly hit the agent's underlying LLM, and continue would synthesize a fake "please continue" turn that confuses CC / Codex. - regenerateMessage: re-create the assistant row branched off the same user message, resolve resume sessionId (drop on cwd mismatch), then spawn a child `execHeterogeneousAgent` op so Stop only kills the executor, not the parent regenerate op. Mirrors sendMessage's hetero branch. - continueGenerationMessage: hetero CLIs have no continue primitive — each prompt is a fresh user turn — so bail out instead of polluting the session. - continueGenerationMessage: gateway mode now branches a server-side resume run instead of falling through to client. Surfaced while testing CC AskUserQuestion end-to-end on the LOBE-8725 branch (regenerating after an answered question went through the wrong runtime). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2 Two bugs surfaced when invoking the local-testing helper from a fresh session on macOS: - `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit code propagates through `pipefail`. With `set -e`, an empty pid set silently kills the whole script — `do_start` reported success, no Electron, no error. Trail with `|| true`. - `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`; process-tree teardown still works because `expand_descendants` walks the tree directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725) `AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across every CC subprocess. The SDK transport latches `_initialized=true` after the first `initialize`, so the second op's CC subprocess sees `Invalid Request: Server already initialized` (400) and reports the `lobe_cc` server as `failed`. From the model's POV the MCP tool is absent — it falls back to ToolSearch, can't find anything, and verbalizes the question instead. Refactor to the canonical multi-tenant pattern: one transport + one `McpServer` per session, looked up by the SDK-managed `mcp-session-id` header. New transports are minted on the first POST without a session id (must be an `initialize` request); subsequent requests route via the stored map; `onsessionclosed` cleans up. The first run of any process still works as before — this only matters once a second op spins up. Added a 3-op sequential regression test that fails on the old single-transport implementation and passes now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725) Step 5's first cut shoehorned the pending form into the Render slot and drove submit/skip with a custom `pluginState.askUserQuestion.status` field, which forced three layers of glue: - `Tool/Detail` had to bypass the loading placeholder via an identifier+apiName hardcode so the form would surface during `isToolCalling` - The executor had to `messageService.getMessages → replaceMessages` after `agent_intervention_request` to drag the freshly-created tool row into in-memory state (the framework's own `tool_end → fetchAndReplaceMessages` only fires after the user answers) - The executor also had to `associateMessageWithOperation` for the tool row so the form could look up the running CC op for IPC All three were patches around skipping the canonical surface. This commit moves AskUserQuestion onto `pluginIntervention.status='pending'` and the `BuiltinToolInterventions` registry, which the framework already drives end-to-end: - `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx` — pure form, no IPC, no store reads. Resolves through the standard `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback. - `Render/AskUserQuestion` shrinks to the answered/aborted view only; the framework hides Render while pending, so no status switching. - New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}" chip in the inline tool body, matching the rest of CC's tools. - Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`; `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry. Hetero needs a different action handler than `submitToolInteraction` (which spawns `executeClientAgent` — wrong for a CC subprocess that's already blocked on an MCP call). Two thin pieces wire that: - `submitHeteroIntervention` (chat store) — sets `pluginIntervention` via `optimisticUpdateMessagePlugin` (which already syncs DB + in-memory + parent-assistant `tools[].intervention` in one shot), then forwards the answer through `heterogeneousAgentService.submitIntervention` IPC. Operation lookup walks the tool message's `parentId` to hit the assistant's `messageOperationMap` entry — drops the explicit `associateMessageWithOperation` call from the executor. - `customInteractionHandlers.isHeteroInteractionIdentifier` flags `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits there before reaching the existing `submitToolInteraction` path. Executor change collapses to one line: `optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`. The post-intervention refresh, the associate call, and the `persistInterventionRequest` helper all go away. Removed: - `AskUserQuestionPluginState` type (custom field is gone) - `Tool/Detail` `askUserPending` inline-render branch - Executor `messageService.getMessages + replaceMessages` round-trip - Executor `associateMessageWithOperation` for tool rows - `persistInterventionRequest` helper Verified end-to-end against a real CC subprocess on desktop: - Inline body shows the new Inspector chip; pending form lives in the bottom InterventionBar (canonical surface) - Submit ships answer through MCP, CC continues with structured result - Skip flips status to `rejected`, framework's RejectedResponse shows "User skipped"; CC receives isError and falls back to text - `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op (the per-session transport fix from the previous commit) - `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725) Select dropdown was the wrong primitive — it hides options behind an extra click and doesn't read like a question to answer. CC's underlying tool is 1-4 questions × 2-4 options, so the whole option set always fits inline. - Each option renders as a clickable card: numbered chip (1/2/3/4) + bold label + secondary description on a single row. Hover tints the background; selected state lights up `colorPrimary` on both the chip and the card outline so the pick is unmistakable at a glance. - Multi-select (`q.multiSelect`) toggles instead of replacing, with a "(multi-select)" hint in the question header. - Multi-question support gets a proper visual hierarchy: each question past the first sits below a dashed divider, headed by a `Q1/N` tag + the original `q.header` chip. The `Q*/N` lets the user track progress without counting. - Inspector picks up the question count too: now shows "askUserQuestion · {first header} +N" when multiple are queued. Verified end-to-end on desktop with a CC-driven 2-question prompt (4-option + 3-option). Both selections feed back to CC as a single "User answers" payload, CC echoes both picks in its continuation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725) - Multi-question forms now use a top tab strip; single question renders inline. - Picking a single-select option auto-advances to the next unanswered question. - Drafts persist to tool message `pluginState.askUserDraft` so picks survive remount / HMR; new `setInterventionDraft` action on the chat store dispatches the pluginState patch. - Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for every unanswered question instead of letting the bridge time out into a cancelled isError — model gets a structured answer it can act on. - Visual: selected option now uses filled `colorPrimaryBg` + right-aligned check icon; index chip stays neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725) The async exit-handler cleanup raced Electron's main-process teardown and left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync unlink in the quit hook is the only reliable guarantee. Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q or `app.quit()`, not on external kills (test harness, OS shutdown). Verified by manual test: pending askUserQuestion forms now leave zero residue after both Cmd+Q and SIGTERM paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725) Submit now writes the structured `{ questionText: pickedLabel(s) }` payload to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so Render no longer has to scrape the bridge's prose `User answers:` content. Render shows one Q&A block per question — header + question + a checkmark card per picked option (multi-select fans out into multiple rows). Falls back to a `—` placeholder when answers are missing (older messages or skipped flows), and keeps the existing `pluginError` warning for cancel / no-answer paths. Also surfaces the answers in the Skill state inspector tab, which was previously empty for completed askUserQuestion messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725) Locks down the regression fixed in c0de0cd — async exit-handler cleanup losing to Electron's main-process teardown. Four cases: `before-quit` (Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown), `SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not throw on the second pass). `process.on` and `process.exit` are stubbed in the signal-path tests so the controller's listener attaches to a spy, not the test runner's process — otherwise we'd leak a real SIGTERM listener every test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
emaxlele
pushed a commit
to emaxlele/lobehub
that referenced
this pull request
May 10, 2026
…obehub#14639) * ✨ feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2) Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's built-in tool short-circuits in `-p` mode, so we host an in-process MCP server that exposes an equivalent `ask_user_question` tool. The handler blocks until the consumer submits an answer (or the 5min deadline / op shutdown fires), surfacing a structured `agent_intervention_request` / `agent_intervention_response` round-trip on the existing event stream. Added in this commit: - `packages/heterogeneous-agents/src/askUser/` - `AskUserBridge` — per-op pending map with timeout / cancel / progress keepalive support; emits an async-iterable of outbound events - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server, `?op=<id>` query routes via `AsyncLocalStorage` → `onsessioninitialized` → sessionId↔opId map; tool handler hands off to the matching bridge and pumps `notifications/progress` back to CC every 30s as wire-level keepalive (required for >5min waits, see spike notes) - `constants.ts` — shared tool/server names + the stable `apiName` the adapter rewrites to - Unit tests cover bridge lifecycle (resolve / cancel / timeout / progress / event stream) and an end-to-end MCP probe via `StreamableHTTPClientTransport` - `packages/agent-gateway-client/src/types.ts` — wire-level `agent_intervention_request` / `agent_intervention_response` event variants + payload interfaces. Re-exported through the package barrel. - `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter rewrites `apiName` to `askUserQuestion` so the renderer routes on a clean domain key. Identifier stays `claude-code`. Applied to both the main-agent and subagent paths for symmetry (subagent ask isn't expected today, but doesn't hurt). - `src/server/routers/lambda/aiAgent.ts` — Zod input schema for `aiAgent.heteroIngest` extended with the two new event types so the CLI sandbox can forward them through the server. No producer wiring yet — Steps 3-5 plug this into Electron main, the renderer executor, and the new UI. * ✨ feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3) Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now goes live during real prompts; renderer-submitted answers route back via new IPC. Changes - `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so controller-managed temp configs flow into the driver. - `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts` — append `--mcp-config <path>` when provided. Disallowed-tools pin stays so CC's built-in AskUserQuestion remains off (avoids double- registration of the same tool name). - `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts` - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt (de-duped concurrent first-callers via in-flight promise). - Per-op `setupInterventionForOp(opId, sessionId)`: registers an `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()` into the existing `heteroAgentEvent` broadcast. - Cleanup paths: exit handler `await intervention.cleanup()` settles pending MCP handlers + unlinks the temp config; pre-spawn errors short-circuit the same cleanup so we don't leak bridges on `buildSpawnPlan` / trace-session failures. - `before-quit` stops the MCP server (in addition to killing CC processes). - New `@IpcMethod() submitIntervention({ operationId, toolCallId, result?, cancelled?, cancelReason? })` — renderer side will dispatch answers / cancellations through this in Step 4/5. - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`. - `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy for `submitIntervention`. - New `claudeCode.test.ts` covers the four driver-arg paths (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay disallowed). Existing 28 controller tests still pass. What still doesn't run end-to-end - The renderer `heteroExecutor` doesn't consume `agent_intervention_request` yet — events go through the broadcast but the chat store ignores them. - No UI to render the intervention card or to call `submitIntervention`. Both lands in Steps 4/5 next. * ✨ feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4) Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId` from MCP `_meta`) instead of a random UUID, so the `agent_intervention_request` event references the same id as the existing tool message on the renderer side. Renderer-side `heteroExecutor` learns the new event: - Added `persistInterventionRequest(...)` next to `persistToolResult` — stamps `pluginState.askUserQuestion` (apiName + identifier + questions parsed from `arguments` + deadline + status='pending' + toolCallId) onto the matching tool message via `messageService.updateToolMessage`. - New branch in `handleStreamEvent` for `'agent_intervention_request'`: defers behind `persistQueue` (so it lands AFTER `persistToolBatch` populates `toolMsgIdByCallId`), then mirrors the same pluginState onto the in-memory message via `internal_dispatchMessage` so the UI lights up immediately — no fetchAndReplaceMessages round-trip needed. - The eventual `tool_result` for the same toolCallId hits the existing `tool_result` branch unchanged: it overwrites `pluginState` with whatever the result carries (typically undefined for our MCP tool, so `pluginState.askUserQuestion` clears and the intervention UI yields to the regular Render). Bridge tests cover the new contract: - caller-supplied toolCallId becomes the wire correlation key - duplicate-toolCallId pendings reject loudly so two-handler clobbers surface immediately 153 package tests + 1167 desktop main tests + 51 hetero executor tests still green; type-check clean. * ✨ feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5) Dedicated Render for the synthetic `askUserQuestion` apiName the adapter rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives under CC's render registry so the existing chat tool-detail flow picks it up automatically — no changes to the conversation framework. - New `AskUserQuestionItem` / `AskUserQuestionArgs` / `AskUserQuestionPluginState` types (mirrors CC's own AskUserQuestion schema verbatim). - `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'` member so the renders / inspectors / streamings registries can key off the same enum value. - `client/Render/AskUserQuestion/index.tsx` is the component: - `pluginState.askUserQuestion?.status === 'pending'` → renders the questions form (Select for single-select, CheckboxGroup for multi-select), a 5-min countdown ticking once a second, Submit / Skip buttons. Reads `operationId` via `messageOperationMap` so we can route through `heterogeneousAgentService.submitIntervention`. - Otherwise → renders the questions as muted captions plus the final answer text from `content`. Surfaces a warning when the tool_result was an error (timeout / cancelled / session ended). - Submit button stays disabled until every question has a selection; Skip always enabled (sends `cancelled: true`). - `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers the new component. What this does NOT do - Doesn't touch `BuiltinToolInterventions` — the form is rendered inside the regular tool body (Render slot), not the canonical intervention slot. Cleanest for now: the framework intervention flow assumes `submitToolInteraction` store actions, which would fight our IPC path. We can refactor onto that surface later if CC grows additional interactions (approval, file picker). - Doesn't translate strings — i18n in a follow-up. Type-check clean. Step 6 (real desktop e2e via CC) is next. * ✨ feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up) Step 5 registered the Render component but stopped at the registry — the chat tool-detail still returned the loading placeholder while `isToolCalling` was true, so users only ever saw a spinner during the 5 min intervention window. Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on CC + apiName=askUserQuestion tool messages) and route to the registered builtin Render inline before the placeholder branch. Once the intervention resolves, the eventual `tool_result` clears `pluginState.askUserQuestion` and the regular Render takes over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up) LOBE-8519 left two TODOs in `generationSlice` where hetero runtime silently fell through to client mode — regenerate would secretly hit the agent's underlying LLM, and continue would synthesize a fake "please continue" turn that confuses CC / Codex. - regenerateMessage: re-create the assistant row branched off the same user message, resolve resume sessionId (drop on cwd mismatch), then spawn a child `execHeterogeneousAgent` op so Stop only kills the executor, not the parent regenerate op. Mirrors sendMessage's hetero branch. - continueGenerationMessage: hetero CLIs have no continue primitive — each prompt is a fresh user turn — so bail out instead of polluting the session. - continueGenerationMessage: gateway mode now branches a server-side resume run instead of falling through to client. Surfaced while testing CC AskUserQuestion end-to-end on the LOBE-8725 branch (regenerating after an answered question went through the wrong runtime). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2 Two bugs surfaced when invoking the local-testing helper from a fresh session on macOS: - `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit code propagates through `pipefail`. With `set -e`, an empty pid set silently kills the whole script — `do_start` reported success, no Electron, no error. Trail with `|| true`. - `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`; process-tree teardown still works because `expand_descendants` walks the tree directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725) `AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across every CC subprocess. The SDK transport latches `_initialized=true` after the first `initialize`, so the second op's CC subprocess sees `Invalid Request: Server already initialized` (400) and reports the `lobe_cc` server as `failed`. From the model's POV the MCP tool is absent — it falls back to ToolSearch, can't find anything, and verbalizes the question instead. Refactor to the canonical multi-tenant pattern: one transport + one `McpServer` per session, looked up by the SDK-managed `mcp-session-id` header. New transports are minted on the first POST without a session id (must be an `initialize` request); subsequent requests route via the stored map; `onsessionclosed` cleans up. The first run of any process still works as before — this only matters once a second op spins up. Added a 3-op sequential regression test that fails on the old single-transport implementation and passes now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725) Step 5's first cut shoehorned the pending form into the Render slot and drove submit/skip with a custom `pluginState.askUserQuestion.status` field, which forced three layers of glue: - `Tool/Detail` had to bypass the loading placeholder via an identifier+apiName hardcode so the form would surface during `isToolCalling` - The executor had to `messageService.getMessages → replaceMessages` after `agent_intervention_request` to drag the freshly-created tool row into in-memory state (the framework's own `tool_end → fetchAndReplaceMessages` only fires after the user answers) - The executor also had to `associateMessageWithOperation` for the tool row so the form could look up the running CC op for IPC All three were patches around skipping the canonical surface. This commit moves AskUserQuestion onto `pluginIntervention.status='pending'` and the `BuiltinToolInterventions` registry, which the framework already drives end-to-end: - `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx` — pure form, no IPC, no store reads. Resolves through the standard `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback. - `Render/AskUserQuestion` shrinks to the answered/aborted view only; the framework hides Render while pending, so no status switching. - New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}" chip in the inline tool body, matching the rest of CC's tools. - Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`; `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry. Hetero needs a different action handler than `submitToolInteraction` (which spawns `executeClientAgent` — wrong for a CC subprocess that's already blocked on an MCP call). Two thin pieces wire that: - `submitHeteroIntervention` (chat store) — sets `pluginIntervention` via `optimisticUpdateMessagePlugin` (which already syncs DB + in-memory + parent-assistant `tools[].intervention` in one shot), then forwards the answer through `heterogeneousAgentService.submitIntervention` IPC. Operation lookup walks the tool message's `parentId` to hit the assistant's `messageOperationMap` entry — drops the explicit `associateMessageWithOperation` call from the executor. - `customInteractionHandlers.isHeteroInteractionIdentifier` flags `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits there before reaching the existing `submitToolInteraction` path. Executor change collapses to one line: `optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`. The post-intervention refresh, the associate call, and the `persistInterventionRequest` helper all go away. Removed: - `AskUserQuestionPluginState` type (custom field is gone) - `Tool/Detail` `askUserPending` inline-render branch - Executor `messageService.getMessages + replaceMessages` round-trip - Executor `associateMessageWithOperation` for tool rows - `persistInterventionRequest` helper Verified end-to-end against a real CC subprocess on desktop: - Inline body shows the new Inspector chip; pending form lives in the bottom InterventionBar (canonical surface) - Submit ships answer through MCP, CC continues with structured result - Skip flips status to `rejected`, framework's RejectedResponse shows "User skipped"; CC receives isError and falls back to text - `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op (the per-session transport fix from the previous commit) - `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725) Select dropdown was the wrong primitive — it hides options behind an extra click and doesn't read like a question to answer. CC's underlying tool is 1-4 questions × 2-4 options, so the whole option set always fits inline. - Each option renders as a clickable card: numbered chip (1/2/3/4) + bold label + secondary description on a single row. Hover tints the background; selected state lights up `colorPrimary` on both the chip and the card outline so the pick is unmistakable at a glance. - Multi-select (`q.multiSelect`) toggles instead of replacing, with a "(multi-select)" hint in the question header. - Multi-question support gets a proper visual hierarchy: each question past the first sits below a dashed divider, headed by a `Q1/N` tag + the original `q.header` chip. The `Q*/N` lets the user track progress without counting. - Inspector picks up the question count too: now shows "askUserQuestion · {first header} +N" when multiple are queued. Verified end-to-end on desktop with a CC-driven 2-question prompt (4-option + 3-option). Both selections feed back to CC as a single "User answers" payload, CC echoes both picks in its continuation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725) - Multi-question forms now use a top tab strip; single question renders inline. - Picking a single-select option auto-advances to the next unanswered question. - Drafts persist to tool message `pluginState.askUserDraft` so picks survive remount / HMR; new `setInterventionDraft` action on the chat store dispatches the pluginState patch. - Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for every unanswered question instead of letting the bridge time out into a cancelled isError — model gets a structured answer it can act on. - Visual: selected option now uses filled `colorPrimaryBg` + right-aligned check icon; index chip stays neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725) The async exit-handler cleanup raced Electron's main-process teardown and left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync unlink in the quit hook is the only reliable guarantee. Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q or `app.quit()`, not on external kills (test harness, OS shutdown). Verified by manual test: pending askUserQuestion forms now leave zero residue after both Cmd+Q and SIGTERM paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725) Submit now writes the structured `{ questionText: pickedLabel(s) }` payload to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so Render no longer has to scrape the bridge's prose `User answers:` content. Render shows one Q&A block per question — header + question + a checkmark card per picked option (multi-select fans out into multiple rows). Falls back to a `—` placeholder when answers are missing (older messages or skipped flows), and keeps the existing `pluginError` warning for cancel / no-answer paths. Also surfaces the answers in the Skill state inspector tab, which was previously empty for completed askUserQuestion messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725) Locks down the regression fixed in c0de0cd — async exit-handler cleanup losing to Electron's main-process teardown. Four cases: `before-quit` (Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown), `SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not throw on the second pass). `process.on` and `process.exit` are stubbed in the signal-path tests so the controller's listener attaches to a spy, not the test runner's process — otherwise we'd leak a real SIGTERM listener every test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
emaxlele
pushed a commit
to emaxlele/lobehub
that referenced
this pull request
May 10, 2026
…obehub#14639) * ✨ feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2) Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's built-in tool short-circuits in `-p` mode, so we host an in-process MCP server that exposes an equivalent `ask_user_question` tool. The handler blocks until the consumer submits an answer (or the 5min deadline / op shutdown fires), surfacing a structured `agent_intervention_request` / `agent_intervention_response` round-trip on the existing event stream. Added in this commit: - `packages/heterogeneous-agents/src/askUser/` - `AskUserBridge` — per-op pending map with timeout / cancel / progress keepalive support; emits an async-iterable of outbound events - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server, `?op=<id>` query routes via `AsyncLocalStorage` → `onsessioninitialized` → sessionId↔opId map; tool handler hands off to the matching bridge and pumps `notifications/progress` back to CC every 30s as wire-level keepalive (required for >5min waits, see spike notes) - `constants.ts` — shared tool/server names + the stable `apiName` the adapter rewrites to - Unit tests cover bridge lifecycle (resolve / cancel / timeout / progress / event stream) and an end-to-end MCP probe via `StreamableHTTPClientTransport` - `packages/agent-gateway-client/src/types.ts` — wire-level `agent_intervention_request` / `agent_intervention_response` event variants + payload interfaces. Re-exported through the package barrel. - `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter rewrites `apiName` to `askUserQuestion` so the renderer routes on a clean domain key. Identifier stays `claude-code`. Applied to both the main-agent and subagent paths for symmetry (subagent ask isn't expected today, but doesn't hurt). - `src/server/routers/lambda/aiAgent.ts` — Zod input schema for `aiAgent.heteroIngest` extended with the two new event types so the CLI sandbox can forward them through the server. No producer wiring yet — Steps 3-5 plug this into Electron main, the renderer executor, and the new UI. * ✨ feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3) Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now goes live during real prompts; renderer-submitted answers route back via new IPC. Changes - `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so controller-managed temp configs flow into the driver. - `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts` — append `--mcp-config <path>` when provided. Disallowed-tools pin stays so CC's built-in AskUserQuestion remains off (avoids double- registration of the same tool name). - `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts` - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt (de-duped concurrent first-callers via in-flight promise). - Per-op `setupInterventionForOp(opId, sessionId)`: registers an `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()` into the existing `heteroAgentEvent` broadcast. - Cleanup paths: exit handler `await intervention.cleanup()` settles pending MCP handlers + unlinks the temp config; pre-spawn errors short-circuit the same cleanup so we don't leak bridges on `buildSpawnPlan` / trace-session failures. - `before-quit` stops the MCP server (in addition to killing CC processes). - New `@IpcMethod() submitIntervention({ operationId, toolCallId, result?, cancelled?, cancelReason? })` — renderer side will dispatch answers / cancellations through this in Step 4/5. - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`. - `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy for `submitIntervention`. - New `claudeCode.test.ts` covers the four driver-arg paths (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay disallowed). Existing 28 controller tests still pass. What still doesn't run end-to-end - The renderer `heteroExecutor` doesn't consume `agent_intervention_request` yet — events go through the broadcast but the chat store ignores them. - No UI to render the intervention card or to call `submitIntervention`. Both lands in Steps 4/5 next. * ✨ feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4) Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId` from MCP `_meta`) instead of a random UUID, so the `agent_intervention_request` event references the same id as the existing tool message on the renderer side. Renderer-side `heteroExecutor` learns the new event: - Added `persistInterventionRequest(...)` next to `persistToolResult` — stamps `pluginState.askUserQuestion` (apiName + identifier + questions parsed from `arguments` + deadline + status='pending' + toolCallId) onto the matching tool message via `messageService.updateToolMessage`. - New branch in `handleStreamEvent` for `'agent_intervention_request'`: defers behind `persistQueue` (so it lands AFTER `persistToolBatch` populates `toolMsgIdByCallId`), then mirrors the same pluginState onto the in-memory message via `internal_dispatchMessage` so the UI lights up immediately — no fetchAndReplaceMessages round-trip needed. - The eventual `tool_result` for the same toolCallId hits the existing `tool_result` branch unchanged: it overwrites `pluginState` with whatever the result carries (typically undefined for our MCP tool, so `pluginState.askUserQuestion` clears and the intervention UI yields to the regular Render). Bridge tests cover the new contract: - caller-supplied toolCallId becomes the wire correlation key - duplicate-toolCallId pendings reject loudly so two-handler clobbers surface immediately 153 package tests + 1167 desktop main tests + 51 hetero executor tests still green; type-check clean. * ✨ feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5) Dedicated Render for the synthetic `askUserQuestion` apiName the adapter rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives under CC's render registry so the existing chat tool-detail flow picks it up automatically — no changes to the conversation framework. - New `AskUserQuestionItem` / `AskUserQuestionArgs` / `AskUserQuestionPluginState` types (mirrors CC's own AskUserQuestion schema verbatim). - `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'` member so the renders / inspectors / streamings registries can key off the same enum value. - `client/Render/AskUserQuestion/index.tsx` is the component: - `pluginState.askUserQuestion?.status === 'pending'` → renders the questions form (Select for single-select, CheckboxGroup for multi-select), a 5-min countdown ticking once a second, Submit / Skip buttons. Reads `operationId` via `messageOperationMap` so we can route through `heterogeneousAgentService.submitIntervention`. - Otherwise → renders the questions as muted captions plus the final answer text from `content`. Surfaces a warning when the tool_result was an error (timeout / cancelled / session ended). - Submit button stays disabled until every question has a selection; Skip always enabled (sends `cancelled: true`). - `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers the new component. What this does NOT do - Doesn't touch `BuiltinToolInterventions` — the form is rendered inside the regular tool body (Render slot), not the canonical intervention slot. Cleanest for now: the framework intervention flow assumes `submitToolInteraction` store actions, which would fight our IPC path. We can refactor onto that surface later if CC grows additional interactions (approval, file picker). - Doesn't translate strings — i18n in a follow-up. Type-check clean. Step 6 (real desktop e2e via CC) is next. * ✨ feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up) Step 5 registered the Render component but stopped at the registry — the chat tool-detail still returned the loading placeholder while `isToolCalling` was true, so users only ever saw a spinner during the 5 min intervention window. Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on CC + apiName=askUserQuestion tool messages) and route to the registered builtin Render inline before the placeholder branch. Once the intervention resolves, the eventual `tool_result` clears `pluginState.askUserQuestion` and the regular Render takes over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up) LOBE-8519 left two TODOs in `generationSlice` where hetero runtime silently fell through to client mode — regenerate would secretly hit the agent's underlying LLM, and continue would synthesize a fake "please continue" turn that confuses CC / Codex. - regenerateMessage: re-create the assistant row branched off the same user message, resolve resume sessionId (drop on cwd mismatch), then spawn a child `execHeterogeneousAgent` op so Stop only kills the executor, not the parent regenerate op. Mirrors sendMessage's hetero branch. - continueGenerationMessage: hetero CLIs have no continue primitive — each prompt is a fresh user turn — so bail out instead of polluting the session. - continueGenerationMessage: gateway mode now branches a server-side resume run instead of falling through to client. Surfaced while testing CC AskUserQuestion end-to-end on the LOBE-8725 branch (regenerating after an answered question went through the wrong runtime). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2 Two bugs surfaced when invoking the local-testing helper from a fresh session on macOS: - `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit code propagates through `pipefail`. With `set -e`, an empty pid set silently kills the whole script — `do_start` reported success, no Electron, no error. Trail with `|| true`. - `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`; process-tree teardown still works because `expand_descendants` walks the tree directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725) `AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across every CC subprocess. The SDK transport latches `_initialized=true` after the first `initialize`, so the second op's CC subprocess sees `Invalid Request: Server already initialized` (400) and reports the `lobe_cc` server as `failed`. From the model's POV the MCP tool is absent — it falls back to ToolSearch, can't find anything, and verbalizes the question instead. Refactor to the canonical multi-tenant pattern: one transport + one `McpServer` per session, looked up by the SDK-managed `mcp-session-id` header. New transports are minted on the first POST without a session id (must be an `initialize` request); subsequent requests route via the stored map; `onsessionclosed` cleans up. The first run of any process still works as before — this only matters once a second op spins up. Added a 3-op sequential regression test that fails on the old single-transport implementation and passes now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725) Step 5's first cut shoehorned the pending form into the Render slot and drove submit/skip with a custom `pluginState.askUserQuestion.status` field, which forced three layers of glue: - `Tool/Detail` had to bypass the loading placeholder via an identifier+apiName hardcode so the form would surface during `isToolCalling` - The executor had to `messageService.getMessages → replaceMessages` after `agent_intervention_request` to drag the freshly-created tool row into in-memory state (the framework's own `tool_end → fetchAndReplaceMessages` only fires after the user answers) - The executor also had to `associateMessageWithOperation` for the tool row so the form could look up the running CC op for IPC All three were patches around skipping the canonical surface. This commit moves AskUserQuestion onto `pluginIntervention.status='pending'` and the `BuiltinToolInterventions` registry, which the framework already drives end-to-end: - `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx` — pure form, no IPC, no store reads. Resolves through the standard `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback. - `Render/AskUserQuestion` shrinks to the answered/aborted view only; the framework hides Render while pending, so no status switching. - New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}" chip in the inline tool body, matching the rest of CC's tools. - Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`; `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry. Hetero needs a different action handler than `submitToolInteraction` (which spawns `executeClientAgent` — wrong for a CC subprocess that's already blocked on an MCP call). Two thin pieces wire that: - `submitHeteroIntervention` (chat store) — sets `pluginIntervention` via `optimisticUpdateMessagePlugin` (which already syncs DB + in-memory + parent-assistant `tools[].intervention` in one shot), then forwards the answer through `heterogeneousAgentService.submitIntervention` IPC. Operation lookup walks the tool message's `parentId` to hit the assistant's `messageOperationMap` entry — drops the explicit `associateMessageWithOperation` call from the executor. - `customInteractionHandlers.isHeteroInteractionIdentifier` flags `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits there before reaching the existing `submitToolInteraction` path. Executor change collapses to one line: `optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`. The post-intervention refresh, the associate call, and the `persistInterventionRequest` helper all go away. Removed: - `AskUserQuestionPluginState` type (custom field is gone) - `Tool/Detail` `askUserPending` inline-render branch - Executor `messageService.getMessages + replaceMessages` round-trip - Executor `associateMessageWithOperation` for tool rows - `persistInterventionRequest` helper Verified end-to-end against a real CC subprocess on desktop: - Inline body shows the new Inspector chip; pending form lives in the bottom InterventionBar (canonical surface) - Submit ships answer through MCP, CC continues with structured result - Skip flips status to `rejected`, framework's RejectedResponse shows "User skipped"; CC receives isError and falls back to text - `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op (the per-session transport fix from the previous commit) - `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725) Select dropdown was the wrong primitive — it hides options behind an extra click and doesn't read like a question to answer. CC's underlying tool is 1-4 questions × 2-4 options, so the whole option set always fits inline. - Each option renders as a clickable card: numbered chip (1/2/3/4) + bold label + secondary description on a single row. Hover tints the background; selected state lights up `colorPrimary` on both the chip and the card outline so the pick is unmistakable at a glance. - Multi-select (`q.multiSelect`) toggles instead of replacing, with a "(multi-select)" hint in the question header. - Multi-question support gets a proper visual hierarchy: each question past the first sits below a dashed divider, headed by a `Q1/N` tag + the original `q.header` chip. The `Q*/N` lets the user track progress without counting. - Inspector picks up the question count too: now shows "askUserQuestion · {first header} +N" when multiple are queued. Verified end-to-end on desktop with a CC-driven 2-question prompt (4-option + 3-option). Both selections feed back to CC as a single "User answers" payload, CC echoes both picks in its continuation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725) - Multi-question forms now use a top tab strip; single question renders inline. - Picking a single-select option auto-advances to the next unanswered question. - Drafts persist to tool message `pluginState.askUserDraft` so picks survive remount / HMR; new `setInterventionDraft` action on the chat store dispatches the pluginState patch. - Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for every unanswered question instead of letting the bridge time out into a cancelled isError — model gets a structured answer it can act on. - Visual: selected option now uses filled `colorPrimaryBg` + right-aligned check icon; index chip stays neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725) The async exit-handler cleanup raced Electron's main-process teardown and left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync unlink in the quit hook is the only reliable guarantee. Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q or `app.quit()`, not on external kills (test harness, OS shutdown). Verified by manual test: pending askUserQuestion forms now leave zero residue after both Cmd+Q and SIGTERM paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725) Submit now writes the structured `{ questionText: pickedLabel(s) }` payload to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so Render no longer has to scrape the bridge's prose `User answers:` content. Render shows one Q&A block per question — header + question + a checkmark card per picked option (multi-select fans out into multiple rows). Falls back to a `—` placeholder when answers are missing (older messages or skipped flows), and keeps the existing `pluginError` warning for cancel / no-answer paths. Also surfaces the answers in the Skill state inspector tab, which was previously empty for completed askUserQuestion messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725) Locks down the regression fixed in c0de0cd — async exit-handler cleanup losing to Electron's main-process teardown. Four cases: `before-quit` (Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown), `SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not throw on the second pass). `process.on` and `process.exit` are stubbed in the signal-path tests so the controller's listener attaches to a spy, not the test runner's process — otherwise we'd leak a real SIGTERM listener every test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
emaxlele
pushed a commit
to emaxlele/lobehub
that referenced
this pull request
May 10, 2026
…obehub#14639) * ✨ feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2) Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's built-in tool short-circuits in `-p` mode, so we host an in-process MCP server that exposes an equivalent `ask_user_question` tool. The handler blocks until the consumer submits an answer (or the 5min deadline / op shutdown fires), surfacing a structured `agent_intervention_request` / `agent_intervention_response` round-trip on the existing event stream. Added in this commit: - `packages/heterogeneous-agents/src/askUser/` - `AskUserBridge` — per-op pending map with timeout / cancel / progress keepalive support; emits an async-iterable of outbound events - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server, `?op=<id>` query routes via `AsyncLocalStorage` → `onsessioninitialized` → sessionId↔opId map; tool handler hands off to the matching bridge and pumps `notifications/progress` back to CC every 30s as wire-level keepalive (required for >5min waits, see spike notes) - `constants.ts` — shared tool/server names + the stable `apiName` the adapter rewrites to - Unit tests cover bridge lifecycle (resolve / cancel / timeout / progress / event stream) and an end-to-end MCP probe via `StreamableHTTPClientTransport` - `packages/agent-gateway-client/src/types.ts` — wire-level `agent_intervention_request` / `agent_intervention_response` event variants + payload interfaces. Re-exported through the package barrel. - `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter rewrites `apiName` to `askUserQuestion` so the renderer routes on a clean domain key. Identifier stays `claude-code`. Applied to both the main-agent and subagent paths for symmetry (subagent ask isn't expected today, but doesn't hurt). - `src/server/routers/lambda/aiAgent.ts` — Zod input schema for `aiAgent.heteroIngest` extended with the two new event types so the CLI sandbox can forward them through the server. No producer wiring yet — Steps 3-5 plug this into Electron main, the renderer executor, and the new UI. * ✨ feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3) Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now goes live during real prompts; renderer-submitted answers route back via new IPC. Changes - `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so controller-managed temp configs flow into the driver. - `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts` — append `--mcp-config <path>` when provided. Disallowed-tools pin stays so CC's built-in AskUserQuestion remains off (avoids double- registration of the same tool name). - `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts` - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt (de-duped concurrent first-callers via in-flight promise). - Per-op `setupInterventionForOp(opId, sessionId)`: registers an `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()` into the existing `heteroAgentEvent` broadcast. - Cleanup paths: exit handler `await intervention.cleanup()` settles pending MCP handlers + unlinks the temp config; pre-spawn errors short-circuit the same cleanup so we don't leak bridges on `buildSpawnPlan` / trace-session failures. - `before-quit` stops the MCP server (in addition to killing CC processes). - New `@IpcMethod() submitIntervention({ operationId, toolCallId, result?, cancelled?, cancelReason? })` — renderer side will dispatch answers / cancellations through this in Step 4/5. - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`. - `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy for `submitIntervention`. - New `claudeCode.test.ts` covers the four driver-arg paths (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay disallowed). Existing 28 controller tests still pass. What still doesn't run end-to-end - The renderer `heteroExecutor` doesn't consume `agent_intervention_request` yet — events go through the broadcast but the chat store ignores them. - No UI to render the intervention card or to call `submitIntervention`. Both lands in Steps 4/5 next. * ✨ feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4) Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId` from MCP `_meta`) instead of a random UUID, so the `agent_intervention_request` event references the same id as the existing tool message on the renderer side. Renderer-side `heteroExecutor` learns the new event: - Added `persistInterventionRequest(...)` next to `persistToolResult` — stamps `pluginState.askUserQuestion` (apiName + identifier + questions parsed from `arguments` + deadline + status='pending' + toolCallId) onto the matching tool message via `messageService.updateToolMessage`. - New branch in `handleStreamEvent` for `'agent_intervention_request'`: defers behind `persistQueue` (so it lands AFTER `persistToolBatch` populates `toolMsgIdByCallId`), then mirrors the same pluginState onto the in-memory message via `internal_dispatchMessage` so the UI lights up immediately — no fetchAndReplaceMessages round-trip needed. - The eventual `tool_result` for the same toolCallId hits the existing `tool_result` branch unchanged: it overwrites `pluginState` with whatever the result carries (typically undefined for our MCP tool, so `pluginState.askUserQuestion` clears and the intervention UI yields to the regular Render). Bridge tests cover the new contract: - caller-supplied toolCallId becomes the wire correlation key - duplicate-toolCallId pendings reject loudly so two-handler clobbers surface immediately 153 package tests + 1167 desktop main tests + 51 hetero executor tests still green; type-check clean. * ✨ feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5) Dedicated Render for the synthetic `askUserQuestion` apiName the adapter rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives under CC's render registry so the existing chat tool-detail flow picks it up automatically — no changes to the conversation framework. - New `AskUserQuestionItem` / `AskUserQuestionArgs` / `AskUserQuestionPluginState` types (mirrors CC's own AskUserQuestion schema verbatim). - `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'` member so the renders / inspectors / streamings registries can key off the same enum value. - `client/Render/AskUserQuestion/index.tsx` is the component: - `pluginState.askUserQuestion?.status === 'pending'` → renders the questions form (Select for single-select, CheckboxGroup for multi-select), a 5-min countdown ticking once a second, Submit / Skip buttons. Reads `operationId` via `messageOperationMap` so we can route through `heterogeneousAgentService.submitIntervention`. - Otherwise → renders the questions as muted captions plus the final answer text from `content`. Surfaces a warning when the tool_result was an error (timeout / cancelled / session ended). - Submit button stays disabled until every question has a selection; Skip always enabled (sends `cancelled: true`). - `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers the new component. What this does NOT do - Doesn't touch `BuiltinToolInterventions` — the form is rendered inside the regular tool body (Render slot), not the canonical intervention slot. Cleanest for now: the framework intervention flow assumes `submitToolInteraction` store actions, which would fight our IPC path. We can refactor onto that surface later if CC grows additional interactions (approval, file picker). - Doesn't translate strings — i18n in a follow-up. Type-check clean. Step 6 (real desktop e2e via CC) is next. * ✨ feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up) Step 5 registered the Render component but stopped at the registry — the chat tool-detail still returned the loading placeholder while `isToolCalling` was true, so users only ever saw a spinner during the 5 min intervention window. Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on CC + apiName=askUserQuestion tool messages) and route to the registered builtin Render inline before the placeholder branch. Once the intervention resolves, the eventual `tool_result` clears `pluginState.askUserQuestion` and the regular Render takes over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up) LOBE-8519 left two TODOs in `generationSlice` where hetero runtime silently fell through to client mode — regenerate would secretly hit the agent's underlying LLM, and continue would synthesize a fake "please continue" turn that confuses CC / Codex. - regenerateMessage: re-create the assistant row branched off the same user message, resolve resume sessionId (drop on cwd mismatch), then spawn a child `execHeterogeneousAgent` op so Stop only kills the executor, not the parent regenerate op. Mirrors sendMessage's hetero branch. - continueGenerationMessage: hetero CLIs have no continue primitive — each prompt is a fresh user turn — so bail out instead of polluting the session. - continueGenerationMessage: gateway mode now branches a server-side resume run instead of falling through to client. Surfaced while testing CC AskUserQuestion end-to-end on the LOBE-8725 branch (regenerating after an answered question went through the wrong runtime). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2 Two bugs surfaced when invoking the local-testing helper from a fresh session on macOS: - `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit code propagates through `pipefail`. With `set -e`, an empty pid set silently kills the whole script — `do_start` reported success, no Electron, no error. Trail with `|| true`. - `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`; process-tree teardown still works because `expand_descendants` walks the tree directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725) `AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across every CC subprocess. The SDK transport latches `_initialized=true` after the first `initialize`, so the second op's CC subprocess sees `Invalid Request: Server already initialized` (400) and reports the `lobe_cc` server as `failed`. From the model's POV the MCP tool is absent — it falls back to ToolSearch, can't find anything, and verbalizes the question instead. Refactor to the canonical multi-tenant pattern: one transport + one `McpServer` per session, looked up by the SDK-managed `mcp-session-id` header. New transports are minted on the first POST without a session id (must be an `initialize` request); subsequent requests route via the stored map; `onsessionclosed` cleans up. The first run of any process still works as before — this only matters once a second op spins up. Added a 3-op sequential regression test that fails on the old single-transport implementation and passes now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725) Step 5's first cut shoehorned the pending form into the Render slot and drove submit/skip with a custom `pluginState.askUserQuestion.status` field, which forced three layers of glue: - `Tool/Detail` had to bypass the loading placeholder via an identifier+apiName hardcode so the form would surface during `isToolCalling` - The executor had to `messageService.getMessages → replaceMessages` after `agent_intervention_request` to drag the freshly-created tool row into in-memory state (the framework's own `tool_end → fetchAndReplaceMessages` only fires after the user answers) - The executor also had to `associateMessageWithOperation` for the tool row so the form could look up the running CC op for IPC All three were patches around skipping the canonical surface. This commit moves AskUserQuestion onto `pluginIntervention.status='pending'` and the `BuiltinToolInterventions` registry, which the framework already drives end-to-end: - `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx` — pure form, no IPC, no store reads. Resolves through the standard `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback. - `Render/AskUserQuestion` shrinks to the answered/aborted view only; the framework hides Render while pending, so no status switching. - New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}" chip in the inline tool body, matching the rest of CC's tools. - Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`; `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry. Hetero needs a different action handler than `submitToolInteraction` (which spawns `executeClientAgent` — wrong for a CC subprocess that's already blocked on an MCP call). Two thin pieces wire that: - `submitHeteroIntervention` (chat store) — sets `pluginIntervention` via `optimisticUpdateMessagePlugin` (which already syncs DB + in-memory + parent-assistant `tools[].intervention` in one shot), then forwards the answer through `heterogeneousAgentService.submitIntervention` IPC. Operation lookup walks the tool message's `parentId` to hit the assistant's `messageOperationMap` entry — drops the explicit `associateMessageWithOperation` call from the executor. - `customInteractionHandlers.isHeteroInteractionIdentifier` flags `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits there before reaching the existing `submitToolInteraction` path. Executor change collapses to one line: `optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`. The post-intervention refresh, the associate call, and the `persistInterventionRequest` helper all go away. Removed: - `AskUserQuestionPluginState` type (custom field is gone) - `Tool/Detail` `askUserPending` inline-render branch - Executor `messageService.getMessages + replaceMessages` round-trip - Executor `associateMessageWithOperation` for tool rows - `persistInterventionRequest` helper Verified end-to-end against a real CC subprocess on desktop: - Inline body shows the new Inspector chip; pending form lives in the bottom InterventionBar (canonical surface) - Submit ships answer through MCP, CC continues with structured result - Skip flips status to `rejected`, framework's RejectedResponse shows "User skipped"; CC receives isError and falls back to text - `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op (the per-session transport fix from the previous commit) - `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725) Select dropdown was the wrong primitive — it hides options behind an extra click and doesn't read like a question to answer. CC's underlying tool is 1-4 questions × 2-4 options, so the whole option set always fits inline. - Each option renders as a clickable card: numbered chip (1/2/3/4) + bold label + secondary description on a single row. Hover tints the background; selected state lights up `colorPrimary` on both the chip and the card outline so the pick is unmistakable at a glance. - Multi-select (`q.multiSelect`) toggles instead of replacing, with a "(multi-select)" hint in the question header. - Multi-question support gets a proper visual hierarchy: each question past the first sits below a dashed divider, headed by a `Q1/N` tag + the original `q.header` chip. The `Q*/N` lets the user track progress without counting. - Inspector picks up the question count too: now shows "askUserQuestion · {first header} +N" when multiple are queued. Verified end-to-end on desktop with a CC-driven 2-question prompt (4-option + 3-option). Both selections feed back to CC as a single "User answers" payload, CC echoes both picks in its continuation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725) - Multi-question forms now use a top tab strip; single question renders inline. - Picking a single-select option auto-advances to the next unanswered question. - Drafts persist to tool message `pluginState.askUserDraft` so picks survive remount / HMR; new `setInterventionDraft` action on the chat store dispatches the pluginState patch. - Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for every unanswered question instead of letting the bridge time out into a cancelled isError — model gets a structured answer it can act on. - Visual: selected option now uses filled `colorPrimaryBg` + right-aligned check icon; index chip stays neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725) The async exit-handler cleanup raced Electron's main-process teardown and left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync unlink in the quit hook is the only reliable guarantee. Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q or `app.quit()`, not on external kills (test harness, OS shutdown). Verified by manual test: pending askUserQuestion forms now leave zero residue after both Cmd+Q and SIGTERM paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725) Submit now writes the structured `{ questionText: pickedLabel(s) }` payload to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so Render no longer has to scrape the bridge's prose `User answers:` content. Render shows one Q&A block per question — header + question + a checkmark card per picked option (multi-select fans out into multiple rows). Falls back to a `—` placeholder when answers are missing (older messages or skipped flows), and keeps the existing `pluginError` warning for cancel / no-answer paths. Also surfaces the answers in the Skill state inspector tab, which was previously empty for completed askUserQuestion messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725) Locks down the regression fixed in c0de0cd — async exit-handler cleanup losing to Electron's main-process teardown. Four cases: `before-quit` (Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown), `SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not throw on the second pass). `process.on` and `process.exit` are stubbed in the signal-path tests so the controller's listener attaches to a spy, not the test runner's process — otherwise we'd leak a real SIGTERM listener every test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
emaxlele
pushed a commit
to emaxlele/lobehub
that referenced
this pull request
May 11, 2026
…obehub#14639) * ✨ feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2) Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's built-in tool short-circuits in `-p` mode, so we host an in-process MCP server that exposes an equivalent `ask_user_question` tool. The handler blocks until the consumer submits an answer (or the 5min deadline / op shutdown fires), surfacing a structured `agent_intervention_request` / `agent_intervention_response` round-trip on the existing event stream. Added in this commit: - `packages/heterogeneous-agents/src/askUser/` - `AskUserBridge` — per-op pending map with timeout / cancel / progress keepalive support; emits an async-iterable of outbound events - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server, `?op=<id>` query routes via `AsyncLocalStorage` → `onsessioninitialized` → sessionId↔opId map; tool handler hands off to the matching bridge and pumps `notifications/progress` back to CC every 30s as wire-level keepalive (required for >5min waits, see spike notes) - `constants.ts` — shared tool/server names + the stable `apiName` the adapter rewrites to - Unit tests cover bridge lifecycle (resolve / cancel / timeout / progress / event stream) and an end-to-end MCP probe via `StreamableHTTPClientTransport` - `packages/agent-gateway-client/src/types.ts` — wire-level `agent_intervention_request` / `agent_intervention_response` event variants + payload interfaces. Re-exported through the package barrel. - `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter rewrites `apiName` to `askUserQuestion` so the renderer routes on a clean domain key. Identifier stays `claude-code`. Applied to both the main-agent and subagent paths for symmetry (subagent ask isn't expected today, but doesn't hurt). - `src/server/routers/lambda/aiAgent.ts` — Zod input schema for `aiAgent.heteroIngest` extended with the two new event types so the CLI sandbox can forward them through the server. No producer wiring yet — Steps 3-5 plug this into Electron main, the renderer executor, and the new UI. * ✨ feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3) Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now goes live during real prompts; renderer-submitted answers route back via new IPC. Changes - `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so controller-managed temp configs flow into the driver. - `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts` — append `--mcp-config <path>` when provided. Disallowed-tools pin stays so CC's built-in AskUserQuestion remains off (avoids double- registration of the same tool name). - `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts` - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt (de-duped concurrent first-callers via in-flight promise). - Per-op `setupInterventionForOp(opId, sessionId)`: registers an `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()` into the existing `heteroAgentEvent` broadcast. - Cleanup paths: exit handler `await intervention.cleanup()` settles pending MCP handlers + unlinks the temp config; pre-spawn errors short-circuit the same cleanup so we don't leak bridges on `buildSpawnPlan` / trace-session failures. - `before-quit` stops the MCP server (in addition to killing CC processes). - New `@IpcMethod() submitIntervention({ operationId, toolCallId, result?, cancelled?, cancelReason? })` — renderer side will dispatch answers / cancellations through this in Step 4/5. - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`. - `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy for `submitIntervention`. - New `claudeCode.test.ts` covers the four driver-arg paths (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay disallowed). Existing 28 controller tests still pass. What still doesn't run end-to-end - The renderer `heteroExecutor` doesn't consume `agent_intervention_request` yet — events go through the broadcast but the chat store ignores them. - No UI to render the intervention card or to call `submitIntervention`. Both lands in Steps 4/5 next. * ✨ feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4) Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId` from MCP `_meta`) instead of a random UUID, so the `agent_intervention_request` event references the same id as the existing tool message on the renderer side. Renderer-side `heteroExecutor` learns the new event: - Added `persistInterventionRequest(...)` next to `persistToolResult` — stamps `pluginState.askUserQuestion` (apiName + identifier + questions parsed from `arguments` + deadline + status='pending' + toolCallId) onto the matching tool message via `messageService.updateToolMessage`. - New branch in `handleStreamEvent` for `'agent_intervention_request'`: defers behind `persistQueue` (so it lands AFTER `persistToolBatch` populates `toolMsgIdByCallId`), then mirrors the same pluginState onto the in-memory message via `internal_dispatchMessage` so the UI lights up immediately — no fetchAndReplaceMessages round-trip needed. - The eventual `tool_result` for the same toolCallId hits the existing `tool_result` branch unchanged: it overwrites `pluginState` with whatever the result carries (typically undefined for our MCP tool, so `pluginState.askUserQuestion` clears and the intervention UI yields to the regular Render). Bridge tests cover the new contract: - caller-supplied toolCallId becomes the wire correlation key - duplicate-toolCallId pendings reject loudly so two-handler clobbers surface immediately 153 package tests + 1167 desktop main tests + 51 hetero executor tests still green; type-check clean. * ✨ feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5) Dedicated Render for the synthetic `askUserQuestion` apiName the adapter rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives under CC's render registry so the existing chat tool-detail flow picks it up automatically — no changes to the conversation framework. - New `AskUserQuestionItem` / `AskUserQuestionArgs` / `AskUserQuestionPluginState` types (mirrors CC's own AskUserQuestion schema verbatim). - `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'` member so the renders / inspectors / streamings registries can key off the same enum value. - `client/Render/AskUserQuestion/index.tsx` is the component: - `pluginState.askUserQuestion?.status === 'pending'` → renders the questions form (Select for single-select, CheckboxGroup for multi-select), a 5-min countdown ticking once a second, Submit / Skip buttons. Reads `operationId` via `messageOperationMap` so we can route through `heterogeneousAgentService.submitIntervention`. - Otherwise → renders the questions as muted captions plus the final answer text from `content`. Surfaces a warning when the tool_result was an error (timeout / cancelled / session ended). - Submit button stays disabled until every question has a selection; Skip always enabled (sends `cancelled: true`). - `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers the new component. What this does NOT do - Doesn't touch `BuiltinToolInterventions` — the form is rendered inside the regular tool body (Render slot), not the canonical intervention slot. Cleanest for now: the framework intervention flow assumes `submitToolInteraction` store actions, which would fight our IPC path. We can refactor onto that surface later if CC grows additional interactions (approval, file picker). - Doesn't translate strings — i18n in a follow-up. Type-check clean. Step 6 (real desktop e2e via CC) is next. * ✨ feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up) Step 5 registered the Render component but stopped at the registry — the chat tool-detail still returned the loading placeholder while `isToolCalling` was true, so users only ever saw a spinner during the 5 min intervention window. Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on CC + apiName=askUserQuestion tool messages) and route to the registered builtin Render inline before the placeholder branch. Once the intervention resolves, the eventual `tool_result` clears `pluginState.askUserQuestion` and the regular Render takes over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up) LOBE-8519 left two TODOs in `generationSlice` where hetero runtime silently fell through to client mode — regenerate would secretly hit the agent's underlying LLM, and continue would synthesize a fake "please continue" turn that confuses CC / Codex. - regenerateMessage: re-create the assistant row branched off the same user message, resolve resume sessionId (drop on cwd mismatch), then spawn a child `execHeterogeneousAgent` op so Stop only kills the executor, not the parent regenerate op. Mirrors sendMessage's hetero branch. - continueGenerationMessage: hetero CLIs have no continue primitive — each prompt is a fresh user turn — so bail out instead of polluting the session. - continueGenerationMessage: gateway mode now branches a server-side resume run instead of falling through to client. Surfaced while testing CC AskUserQuestion end-to-end on the LOBE-8725 branch (regenerating after an answered question went through the wrong runtime). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2 Two bugs surfaced when invoking the local-testing helper from a fresh session on macOS: - `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit code propagates through `pipefail`. With `set -e`, an empty pid set silently kills the whole script — `do_start` reported success, no Electron, no error. Trail with `|| true`. - `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`; process-tree teardown still works because `expand_descendants` walks the tree directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725) `AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across every CC subprocess. The SDK transport latches `_initialized=true` after the first `initialize`, so the second op's CC subprocess sees `Invalid Request: Server already initialized` (400) and reports the `lobe_cc` server as `failed`. From the model's POV the MCP tool is absent — it falls back to ToolSearch, can't find anything, and verbalizes the question instead. Refactor to the canonical multi-tenant pattern: one transport + one `McpServer` per session, looked up by the SDK-managed `mcp-session-id` header. New transports are minted on the first POST without a session id (must be an `initialize` request); subsequent requests route via the stored map; `onsessionclosed` cleans up. The first run of any process still works as before — this only matters once a second op spins up. Added a 3-op sequential regression test that fails on the old single-transport implementation and passes now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725) Step 5's first cut shoehorned the pending form into the Render slot and drove submit/skip with a custom `pluginState.askUserQuestion.status` field, which forced three layers of glue: - `Tool/Detail` had to bypass the loading placeholder via an identifier+apiName hardcode so the form would surface during `isToolCalling` - The executor had to `messageService.getMessages → replaceMessages` after `agent_intervention_request` to drag the freshly-created tool row into in-memory state (the framework's own `tool_end → fetchAndReplaceMessages` only fires after the user answers) - The executor also had to `associateMessageWithOperation` for the tool row so the form could look up the running CC op for IPC All three were patches around skipping the canonical surface. This commit moves AskUserQuestion onto `pluginIntervention.status='pending'` and the `BuiltinToolInterventions` registry, which the framework already drives end-to-end: - `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx` — pure form, no IPC, no store reads. Resolves through the standard `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback. - `Render/AskUserQuestion` shrinks to the answered/aborted view only; the framework hides Render while pending, so no status switching. - New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}" chip in the inline tool body, matching the rest of CC's tools. - Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`; `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry. Hetero needs a different action handler than `submitToolInteraction` (which spawns `executeClientAgent` — wrong for a CC subprocess that's already blocked on an MCP call). Two thin pieces wire that: - `submitHeteroIntervention` (chat store) — sets `pluginIntervention` via `optimisticUpdateMessagePlugin` (which already syncs DB + in-memory + parent-assistant `tools[].intervention` in one shot), then forwards the answer through `heterogeneousAgentService.submitIntervention` IPC. Operation lookup walks the tool message's `parentId` to hit the assistant's `messageOperationMap` entry — drops the explicit `associateMessageWithOperation` call from the executor. - `customInteractionHandlers.isHeteroInteractionIdentifier` flags `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits there before reaching the existing `submitToolInteraction` path. Executor change collapses to one line: `optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`. The post-intervention refresh, the associate call, and the `persistInterventionRequest` helper all go away. Removed: - `AskUserQuestionPluginState` type (custom field is gone) - `Tool/Detail` `askUserPending` inline-render branch - Executor `messageService.getMessages + replaceMessages` round-trip - Executor `associateMessageWithOperation` for tool rows - `persistInterventionRequest` helper Verified end-to-end against a real CC subprocess on desktop: - Inline body shows the new Inspector chip; pending form lives in the bottom InterventionBar (canonical surface) - Submit ships answer through MCP, CC continues with structured result - Skip flips status to `rejected`, framework's RejectedResponse shows "User skipped"; CC receives isError and falls back to text - `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op (the per-session transport fix from the previous commit) - `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725) Select dropdown was the wrong primitive — it hides options behind an extra click and doesn't read like a question to answer. CC's underlying tool is 1-4 questions × 2-4 options, so the whole option set always fits inline. - Each option renders as a clickable card: numbered chip (1/2/3/4) + bold label + secondary description on a single row. Hover tints the background; selected state lights up `colorPrimary` on both the chip and the card outline so the pick is unmistakable at a glance. - Multi-select (`q.multiSelect`) toggles instead of replacing, with a "(multi-select)" hint in the question header. - Multi-question support gets a proper visual hierarchy: each question past the first sits below a dashed divider, headed by a `Q1/N` tag + the original `q.header` chip. The `Q*/N` lets the user track progress without counting. - Inspector picks up the question count too: now shows "askUserQuestion · {first header} +N" when multiple are queued. Verified end-to-end on desktop with a CC-driven 2-question prompt (4-option + 3-option). Both selections feed back to CC as a single "User answers" payload, CC echoes both picks in its continuation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725) - Multi-question forms now use a top tab strip; single question renders inline. - Picking a single-select option auto-advances to the next unanswered question. - Drafts persist to tool message `pluginState.askUserDraft` so picks survive remount / HMR; new `setInterventionDraft` action on the chat store dispatches the pluginState patch. - Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for every unanswered question instead of letting the bridge time out into a cancelled isError — model gets a structured answer it can act on. - Visual: selected option now uses filled `colorPrimaryBg` + right-aligned check icon; index chip stays neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725) The async exit-handler cleanup raced Electron's main-process teardown and left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync unlink in the quit hook is the only reliable guarantee. Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q or `app.quit()`, not on external kills (test harness, OS shutdown). Verified by manual test: pending askUserQuestion forms now leave zero residue after both Cmd+Q and SIGTERM paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725) Submit now writes the structured `{ questionText: pickedLabel(s) }` payload to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so Render no longer has to scrape the bridge's prose `User answers:` content. Render shows one Q&A block per question — header + question + a checkmark card per picked option (multi-select fans out into multiple rows). Falls back to a `—` placeholder when answers are missing (older messages or skipped flows), and keeps the existing `pluginError` warning for cancel / no-answer paths. Also surfaces the answers in the Skill state inspector tab, which was previously empty for completed askUserQuestion messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725) Locks down the regression fixed in c0de0cd — async exit-handler cleanup losing to Electron's main-process teardown. Four cases: `before-quit` (Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown), `SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not throw on the second pass). `process.on` and `process.exit` are stubbed in the signal-path tests so the controller's listener attaches to a spy, not the test runner's process — otherwise we'd leak a real SIGTERM listener every test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
emaxlele
pushed a commit
to emaxlele/lobehub
that referenced
this pull request
May 11, 2026
…obehub#14639) * ✨ feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2) Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's built-in tool short-circuits in `-p` mode, so we host an in-process MCP server that exposes an equivalent `ask_user_question` tool. The handler blocks until the consumer submits an answer (or the 5min deadline / op shutdown fires), surfacing a structured `agent_intervention_request` / `agent_intervention_response` round-trip on the existing event stream. Added in this commit: - `packages/heterogeneous-agents/src/askUser/` - `AskUserBridge` — per-op pending map with timeout / cancel / progress keepalive support; emits an async-iterable of outbound events - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server, `?op=<id>` query routes via `AsyncLocalStorage` → `onsessioninitialized` → sessionId↔opId map; tool handler hands off to the matching bridge and pumps `notifications/progress` back to CC every 30s as wire-level keepalive (required for >5min waits, see spike notes) - `constants.ts` — shared tool/server names + the stable `apiName` the adapter rewrites to - Unit tests cover bridge lifecycle (resolve / cancel / timeout / progress / event stream) and an end-to-end MCP probe via `StreamableHTTPClientTransport` - `packages/agent-gateway-client/src/types.ts` — wire-level `agent_intervention_request` / `agent_intervention_response` event variants + payload interfaces. Re-exported through the package barrel. - `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter rewrites `apiName` to `askUserQuestion` so the renderer routes on a clean domain key. Identifier stays `claude-code`. Applied to both the main-agent and subagent paths for symmetry (subagent ask isn't expected today, but doesn't hurt). - `src/server/routers/lambda/aiAgent.ts` — Zod input schema for `aiAgent.heteroIngest` extended with the two new event types so the CLI sandbox can forward them through the server. No producer wiring yet — Steps 3-5 plug this into Electron main, the renderer executor, and the new UI. * ✨ feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3) Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now goes live during real prompts; renderer-submitted answers route back via new IPC. Changes - `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so controller-managed temp configs flow into the driver. - `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts` — append `--mcp-config <path>` when provided. Disallowed-tools pin stays so CC's built-in AskUserQuestion remains off (avoids double- registration of the same tool name). - `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts` - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt (de-duped concurrent first-callers via in-flight promise). - Per-op `setupInterventionForOp(opId, sessionId)`: registers an `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()` into the existing `heteroAgentEvent` broadcast. - Cleanup paths: exit handler `await intervention.cleanup()` settles pending MCP handlers + unlinks the temp config; pre-spawn errors short-circuit the same cleanup so we don't leak bridges on `buildSpawnPlan` / trace-session failures. - `before-quit` stops the MCP server (in addition to killing CC processes). - New `@IpcMethod() submitIntervention({ operationId, toolCallId, result?, cancelled?, cancelReason? })` — renderer side will dispatch answers / cancellations through this in Step 4/5. - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`. - `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy for `submitIntervention`. - New `claudeCode.test.ts` covers the four driver-arg paths (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay disallowed). Existing 28 controller tests still pass. What still doesn't run end-to-end - The renderer `heteroExecutor` doesn't consume `agent_intervention_request` yet — events go through the broadcast but the chat store ignores them. - No UI to render the intervention card or to call `submitIntervention`. Both lands in Steps 4/5 next. * ✨ feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4) Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId` from MCP `_meta`) instead of a random UUID, so the `agent_intervention_request` event references the same id as the existing tool message on the renderer side. Renderer-side `heteroExecutor` learns the new event: - Added `persistInterventionRequest(...)` next to `persistToolResult` — stamps `pluginState.askUserQuestion` (apiName + identifier + questions parsed from `arguments` + deadline + status='pending' + toolCallId) onto the matching tool message via `messageService.updateToolMessage`. - New branch in `handleStreamEvent` for `'agent_intervention_request'`: defers behind `persistQueue` (so it lands AFTER `persistToolBatch` populates `toolMsgIdByCallId`), then mirrors the same pluginState onto the in-memory message via `internal_dispatchMessage` so the UI lights up immediately — no fetchAndReplaceMessages round-trip needed. - The eventual `tool_result` for the same toolCallId hits the existing `tool_result` branch unchanged: it overwrites `pluginState` with whatever the result carries (typically undefined for our MCP tool, so `pluginState.askUserQuestion` clears and the intervention UI yields to the regular Render). Bridge tests cover the new contract: - caller-supplied toolCallId becomes the wire correlation key - duplicate-toolCallId pendings reject loudly so two-handler clobbers surface immediately 153 package tests + 1167 desktop main tests + 51 hetero executor tests still green; type-check clean. * ✨ feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5) Dedicated Render for the synthetic `askUserQuestion` apiName the adapter rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives under CC's render registry so the existing chat tool-detail flow picks it up automatically — no changes to the conversation framework. - New `AskUserQuestionItem` / `AskUserQuestionArgs` / `AskUserQuestionPluginState` types (mirrors CC's own AskUserQuestion schema verbatim). - `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'` member so the renders / inspectors / streamings registries can key off the same enum value. - `client/Render/AskUserQuestion/index.tsx` is the component: - `pluginState.askUserQuestion?.status === 'pending'` → renders the questions form (Select for single-select, CheckboxGroup for multi-select), a 5-min countdown ticking once a second, Submit / Skip buttons. Reads `operationId` via `messageOperationMap` so we can route through `heterogeneousAgentService.submitIntervention`. - Otherwise → renders the questions as muted captions plus the final answer text from `content`. Surfaces a warning when the tool_result was an error (timeout / cancelled / session ended). - Submit button stays disabled until every question has a selection; Skip always enabled (sends `cancelled: true`). - `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers the new component. What this does NOT do - Doesn't touch `BuiltinToolInterventions` — the form is rendered inside the regular tool body (Render slot), not the canonical intervention slot. Cleanest for now: the framework intervention flow assumes `submitToolInteraction` store actions, which would fight our IPC path. We can refactor onto that surface later if CC grows additional interactions (approval, file picker). - Doesn't translate strings — i18n in a follow-up. Type-check clean. Step 6 (real desktop e2e via CC) is next. * ✨ feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up) Step 5 registered the Render component but stopped at the registry — the chat tool-detail still returned the loading placeholder while `isToolCalling` was true, so users only ever saw a spinner during the 5 min intervention window. Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on CC + apiName=askUserQuestion tool messages) and route to the registered builtin Render inline before the placeholder branch. Once the intervention resolves, the eventual `tool_result` clears `pluginState.askUserQuestion` and the regular Render takes over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up) LOBE-8519 left two TODOs in `generationSlice` where hetero runtime silently fell through to client mode — regenerate would secretly hit the agent's underlying LLM, and continue would synthesize a fake "please continue" turn that confuses CC / Codex. - regenerateMessage: re-create the assistant row branched off the same user message, resolve resume sessionId (drop on cwd mismatch), then spawn a child `execHeterogeneousAgent` op so Stop only kills the executor, not the parent regenerate op. Mirrors sendMessage's hetero branch. - continueGenerationMessage: hetero CLIs have no continue primitive — each prompt is a fresh user turn — so bail out instead of polluting the session. - continueGenerationMessage: gateway mode now branches a server-side resume run instead of falling through to client. Surfaced while testing CC AskUserQuestion end-to-end on the LOBE-8725 branch (regenerating after an answered question went through the wrong runtime). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2 Two bugs surfaced when invoking the local-testing helper from a fresh session on macOS: - `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit code propagates through `pipefail`. With `set -e`, an empty pid set silently kills the whole script — `do_start` reported success, no Electron, no error. Trail with `|| true`. - `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`; process-tree teardown still works because `expand_descendants` walks the tree directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725) `AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across every CC subprocess. The SDK transport latches `_initialized=true` after the first `initialize`, so the second op's CC subprocess sees `Invalid Request: Server already initialized` (400) and reports the `lobe_cc` server as `failed`. From the model's POV the MCP tool is absent — it falls back to ToolSearch, can't find anything, and verbalizes the question instead. Refactor to the canonical multi-tenant pattern: one transport + one `McpServer` per session, looked up by the SDK-managed `mcp-session-id` header. New transports are minted on the first POST without a session id (must be an `initialize` request); subsequent requests route via the stored map; `onsessionclosed` cleans up. The first run of any process still works as before — this only matters once a second op spins up. Added a 3-op sequential regression test that fails on the old single-transport implementation and passes now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725) Step 5's first cut shoehorned the pending form into the Render slot and drove submit/skip with a custom `pluginState.askUserQuestion.status` field, which forced three layers of glue: - `Tool/Detail` had to bypass the loading placeholder via an identifier+apiName hardcode so the form would surface during `isToolCalling` - The executor had to `messageService.getMessages → replaceMessages` after `agent_intervention_request` to drag the freshly-created tool row into in-memory state (the framework's own `tool_end → fetchAndReplaceMessages` only fires after the user answers) - The executor also had to `associateMessageWithOperation` for the tool row so the form could look up the running CC op for IPC All three were patches around skipping the canonical surface. This commit moves AskUserQuestion onto `pluginIntervention.status='pending'` and the `BuiltinToolInterventions` registry, which the framework already drives end-to-end: - `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx` — pure form, no IPC, no store reads. Resolves through the standard `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback. - `Render/AskUserQuestion` shrinks to the answered/aborted view only; the framework hides Render while pending, so no status switching. - New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}" chip in the inline tool body, matching the rest of CC's tools. - Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`; `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry. Hetero needs a different action handler than `submitToolInteraction` (which spawns `executeClientAgent` — wrong for a CC subprocess that's already blocked on an MCP call). Two thin pieces wire that: - `submitHeteroIntervention` (chat store) — sets `pluginIntervention` via `optimisticUpdateMessagePlugin` (which already syncs DB + in-memory + parent-assistant `tools[].intervention` in one shot), then forwards the answer through `heterogeneousAgentService.submitIntervention` IPC. Operation lookup walks the tool message's `parentId` to hit the assistant's `messageOperationMap` entry — drops the explicit `associateMessageWithOperation` call from the executor. - `customInteractionHandlers.isHeteroInteractionIdentifier` flags `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits there before reaching the existing `submitToolInteraction` path. Executor change collapses to one line: `optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`. The post-intervention refresh, the associate call, and the `persistInterventionRequest` helper all go away. Removed: - `AskUserQuestionPluginState` type (custom field is gone) - `Tool/Detail` `askUserPending` inline-render branch - Executor `messageService.getMessages + replaceMessages` round-trip - Executor `associateMessageWithOperation` for tool rows - `persistInterventionRequest` helper Verified end-to-end against a real CC subprocess on desktop: - Inline body shows the new Inspector chip; pending form lives in the bottom InterventionBar (canonical surface) - Submit ships answer through MCP, CC continues with structured result - Skip flips status to `rejected`, framework's RejectedResponse shows "User skipped"; CC receives isError and falls back to text - `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op (the per-session transport fix from the previous commit) - `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725) Select dropdown was the wrong primitive — it hides options behind an extra click and doesn't read like a question to answer. CC's underlying tool is 1-4 questions × 2-4 options, so the whole option set always fits inline. - Each option renders as a clickable card: numbered chip (1/2/3/4) + bold label + secondary description on a single row. Hover tints the background; selected state lights up `colorPrimary` on both the chip and the card outline so the pick is unmistakable at a glance. - Multi-select (`q.multiSelect`) toggles instead of replacing, with a "(multi-select)" hint in the question header. - Multi-question support gets a proper visual hierarchy: each question past the first sits below a dashed divider, headed by a `Q1/N` tag + the original `q.header` chip. The `Q*/N` lets the user track progress without counting. - Inspector picks up the question count too: now shows "askUserQuestion · {first header} +N" when multiple are queued. Verified end-to-end on desktop with a CC-driven 2-question prompt (4-option + 3-option). Both selections feed back to CC as a single "User answers" payload, CC echoes both picks in its continuation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725) - Multi-question forms now use a top tab strip; single question renders inline. - Picking a single-select option auto-advances to the next unanswered question. - Drafts persist to tool message `pluginState.askUserDraft` so picks survive remount / HMR; new `setInterventionDraft` action on the chat store dispatches the pluginState patch. - Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for every unanswered question instead of letting the bridge time out into a cancelled isError — model gets a structured answer it can act on. - Visual: selected option now uses filled `colorPrimaryBg` + right-aligned check icon; index chip stays neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725) The async exit-handler cleanup raced Electron's main-process teardown and left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync unlink in the quit hook is the only reliable guarantee. Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q or `app.quit()`, not on external kills (test harness, OS shutdown). Verified by manual test: pending askUserQuestion forms now leave zero residue after both Cmd+Q and SIGTERM paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725) Submit now writes the structured `{ questionText: pickedLabel(s) }` payload to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so Render no longer has to scrape the bridge's prose `User answers:` content. Render shows one Q&A block per question — header + question + a checkmark card per picked option (multi-select fans out into multiple rows). Falls back to a `—` placeholder when answers are missing (older messages or skipped flows), and keeps the existing `pluginError` warning for cancel / no-answer paths. Also surfaces the answers in the Skill state inspector tab, which was previously empty for completed askUserQuestion messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725) Locks down the regression fixed in c0de0cd — async exit-handler cleanup losing to Electron's main-process teardown. Four cases: `before-quit` (Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown), `SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not throw on the second pass). `process.on` and `process.exit` are stubbed in the signal-path tests so the controller's listener attaches to a spy, not the test runner's process — otherwise we'd leak a real SIGTERM listener every test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
emaxlele
pushed a commit
to emaxlele/lobehub
that referenced
this pull request
May 11, 2026
…obehub#14639) * ✨ feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2) Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's built-in tool short-circuits in `-p` mode, so we host an in-process MCP server that exposes an equivalent `ask_user_question` tool. The handler blocks until the consumer submits an answer (or the 5min deadline / op shutdown fires), surfacing a structured `agent_intervention_request` / `agent_intervention_response` round-trip on the existing event stream. Added in this commit: - `packages/heterogeneous-agents/src/askUser/` - `AskUserBridge` — per-op pending map with timeout / cancel / progress keepalive support; emits an async-iterable of outbound events - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server, `?op=<id>` query routes via `AsyncLocalStorage` → `onsessioninitialized` → sessionId↔opId map; tool handler hands off to the matching bridge and pumps `notifications/progress` back to CC every 30s as wire-level keepalive (required for >5min waits, see spike notes) - `constants.ts` — shared tool/server names + the stable `apiName` the adapter rewrites to - Unit tests cover bridge lifecycle (resolve / cancel / timeout / progress / event stream) and an end-to-end MCP probe via `StreamableHTTPClientTransport` - `packages/agent-gateway-client/src/types.ts` — wire-level `agent_intervention_request` / `agent_intervention_response` event variants + payload interfaces. Re-exported through the package barrel. - `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter rewrites `apiName` to `askUserQuestion` so the renderer routes on a clean domain key. Identifier stays `claude-code`. Applied to both the main-agent and subagent paths for symmetry (subagent ask isn't expected today, but doesn't hurt). - `src/server/routers/lambda/aiAgent.ts` — Zod input schema for `aiAgent.heteroIngest` extended with the two new event types so the CLI sandbox can forward them through the server. No producer wiring yet — Steps 3-5 plug this into Electron main, the renderer executor, and the new UI. * ✨ feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3) Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now goes live during real prompts; renderer-submitted answers route back via new IPC. Changes - `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so controller-managed temp configs flow into the driver. - `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts` — append `--mcp-config <path>` when provided. Disallowed-tools pin stays so CC's built-in AskUserQuestion remains off (avoids double- registration of the same tool name). - `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts` - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt (de-duped concurrent first-callers via in-flight promise). - Per-op `setupInterventionForOp(opId, sessionId)`: registers an `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()` into the existing `heteroAgentEvent` broadcast. - Cleanup paths: exit handler `await intervention.cleanup()` settles pending MCP handlers + unlinks the temp config; pre-spawn errors short-circuit the same cleanup so we don't leak bridges on `buildSpawnPlan` / trace-session failures. - `before-quit` stops the MCP server (in addition to killing CC processes). - New `@IpcMethod() submitIntervention({ operationId, toolCallId, result?, cancelled?, cancelReason? })` — renderer side will dispatch answers / cancellations through this in Step 4/5. - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`. - `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy for `submitIntervention`. - New `claudeCode.test.ts` covers the four driver-arg paths (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay disallowed). Existing 28 controller tests still pass. What still doesn't run end-to-end - The renderer `heteroExecutor` doesn't consume `agent_intervention_request` yet — events go through the broadcast but the chat store ignores them. - No UI to render the intervention card or to call `submitIntervention`. Both lands in Steps 4/5 next. * ✨ feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4) Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId` from MCP `_meta`) instead of a random UUID, so the `agent_intervention_request` event references the same id as the existing tool message on the renderer side. Renderer-side `heteroExecutor` learns the new event: - Added `persistInterventionRequest(...)` next to `persistToolResult` — stamps `pluginState.askUserQuestion` (apiName + identifier + questions parsed from `arguments` + deadline + status='pending' + toolCallId) onto the matching tool message via `messageService.updateToolMessage`. - New branch in `handleStreamEvent` for `'agent_intervention_request'`: defers behind `persistQueue` (so it lands AFTER `persistToolBatch` populates `toolMsgIdByCallId`), then mirrors the same pluginState onto the in-memory message via `internal_dispatchMessage` so the UI lights up immediately — no fetchAndReplaceMessages round-trip needed. - The eventual `tool_result` for the same toolCallId hits the existing `tool_result` branch unchanged: it overwrites `pluginState` with whatever the result carries (typically undefined for our MCP tool, so `pluginState.askUserQuestion` clears and the intervention UI yields to the regular Render). Bridge tests cover the new contract: - caller-supplied toolCallId becomes the wire correlation key - duplicate-toolCallId pendings reject loudly so two-handler clobbers surface immediately 153 package tests + 1167 desktop main tests + 51 hetero executor tests still green; type-check clean. * ✨ feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5) Dedicated Render for the synthetic `askUserQuestion` apiName the adapter rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives under CC's render registry so the existing chat tool-detail flow picks it up automatically — no changes to the conversation framework. - New `AskUserQuestionItem` / `AskUserQuestionArgs` / `AskUserQuestionPluginState` types (mirrors CC's own AskUserQuestion schema verbatim). - `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'` member so the renders / inspectors / streamings registries can key off the same enum value. - `client/Render/AskUserQuestion/index.tsx` is the component: - `pluginState.askUserQuestion?.status === 'pending'` → renders the questions form (Select for single-select, CheckboxGroup for multi-select), a 5-min countdown ticking once a second, Submit / Skip buttons. Reads `operationId` via `messageOperationMap` so we can route through `heterogeneousAgentService.submitIntervention`. - Otherwise → renders the questions as muted captions plus the final answer text from `content`. Surfaces a warning when the tool_result was an error (timeout / cancelled / session ended). - Submit button stays disabled until every question has a selection; Skip always enabled (sends `cancelled: true`). - `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers the new component. What this does NOT do - Doesn't touch `BuiltinToolInterventions` — the form is rendered inside the regular tool body (Render slot), not the canonical intervention slot. Cleanest for now: the framework intervention flow assumes `submitToolInteraction` store actions, which would fight our IPC path. We can refactor onto that surface later if CC grows additional interactions (approval, file picker). - Doesn't translate strings — i18n in a follow-up. Type-check clean. Step 6 (real desktop e2e via CC) is next. * ✨ feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up) Step 5 registered the Render component but stopped at the registry — the chat tool-detail still returned the loading placeholder while `isToolCalling` was true, so users only ever saw a spinner during the 5 min intervention window. Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on CC + apiName=askUserQuestion tool messages) and route to the registered builtin Render inline before the placeholder branch. Once the intervention resolves, the eventual `tool_result` clears `pluginState.askUserQuestion` and the regular Render takes over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up) LOBE-8519 left two TODOs in `generationSlice` where hetero runtime silently fell through to client mode — regenerate would secretly hit the agent's underlying LLM, and continue would synthesize a fake "please continue" turn that confuses CC / Codex. - regenerateMessage: re-create the assistant row branched off the same user message, resolve resume sessionId (drop on cwd mismatch), then spawn a child `execHeterogeneousAgent` op so Stop only kills the executor, not the parent regenerate op. Mirrors sendMessage's hetero branch. - continueGenerationMessage: hetero CLIs have no continue primitive — each prompt is a fresh user turn — so bail out instead of polluting the session. - continueGenerationMessage: gateway mode now branches a server-side resume run instead of falling through to client. Surfaced while testing CC AskUserQuestion end-to-end on the LOBE-8725 branch (regenerating after an answered question went through the wrong runtime). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2 Two bugs surfaced when invoking the local-testing helper from a fresh session on macOS: - `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit code propagates through `pipefail`. With `set -e`, an empty pid set silently kills the whole script — `do_start` reported success, no Electron, no error. Trail with `|| true`. - `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`; process-tree teardown still works because `expand_descendants` walks the tree directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725) `AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across every CC subprocess. The SDK transport latches `_initialized=true` after the first `initialize`, so the second op's CC subprocess sees `Invalid Request: Server already initialized` (400) and reports the `lobe_cc` server as `failed`. From the model's POV the MCP tool is absent — it falls back to ToolSearch, can't find anything, and verbalizes the question instead. Refactor to the canonical multi-tenant pattern: one transport + one `McpServer` per session, looked up by the SDK-managed `mcp-session-id` header. New transports are minted on the first POST without a session id (must be an `initialize` request); subsequent requests route via the stored map; `onsessionclosed` cleans up. The first run of any process still works as before — this only matters once a second op spins up. Added a 3-op sequential regression test that fails on the old single-transport implementation and passes now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725) Step 5's first cut shoehorned the pending form into the Render slot and drove submit/skip with a custom `pluginState.askUserQuestion.status` field, which forced three layers of glue: - `Tool/Detail` had to bypass the loading placeholder via an identifier+apiName hardcode so the form would surface during `isToolCalling` - The executor had to `messageService.getMessages → replaceMessages` after `agent_intervention_request` to drag the freshly-created tool row into in-memory state (the framework's own `tool_end → fetchAndReplaceMessages` only fires after the user answers) - The executor also had to `associateMessageWithOperation` for the tool row so the form could look up the running CC op for IPC All three were patches around skipping the canonical surface. This commit moves AskUserQuestion onto `pluginIntervention.status='pending'` and the `BuiltinToolInterventions` registry, which the framework already drives end-to-end: - `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx` — pure form, no IPC, no store reads. Resolves through the standard `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback. - `Render/AskUserQuestion` shrinks to the answered/aborted view only; the framework hides Render while pending, so no status switching. - New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}" chip in the inline tool body, matching the rest of CC's tools. - Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`; `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry. Hetero needs a different action handler than `submitToolInteraction` (which spawns `executeClientAgent` — wrong for a CC subprocess that's already blocked on an MCP call). Two thin pieces wire that: - `submitHeteroIntervention` (chat store) — sets `pluginIntervention` via `optimisticUpdateMessagePlugin` (which already syncs DB + in-memory + parent-assistant `tools[].intervention` in one shot), then forwards the answer through `heterogeneousAgentService.submitIntervention` IPC. Operation lookup walks the tool message's `parentId` to hit the assistant's `messageOperationMap` entry — drops the explicit `associateMessageWithOperation` call from the executor. - `customInteractionHandlers.isHeteroInteractionIdentifier` flags `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits there before reaching the existing `submitToolInteraction` path. Executor change collapses to one line: `optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`. The post-intervention refresh, the associate call, and the `persistInterventionRequest` helper all go away. Removed: - `AskUserQuestionPluginState` type (custom field is gone) - `Tool/Detail` `askUserPending` inline-render branch - Executor `messageService.getMessages + replaceMessages` round-trip - Executor `associateMessageWithOperation` for tool rows - `persistInterventionRequest` helper Verified end-to-end against a real CC subprocess on desktop: - Inline body shows the new Inspector chip; pending form lives in the bottom InterventionBar (canonical surface) - Submit ships answer through MCP, CC continues with structured result - Skip flips status to `rejected`, framework's RejectedResponse shows "User skipped"; CC receives isError and falls back to text - `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op (the per-session transport fix from the previous commit) - `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725) Select dropdown was the wrong primitive — it hides options behind an extra click and doesn't read like a question to answer. CC's underlying tool is 1-4 questions × 2-4 options, so the whole option set always fits inline. - Each option renders as a clickable card: numbered chip (1/2/3/4) + bold label + secondary description on a single row. Hover tints the background; selected state lights up `colorPrimary` on both the chip and the card outline so the pick is unmistakable at a glance. - Multi-select (`q.multiSelect`) toggles instead of replacing, with a "(multi-select)" hint in the question header. - Multi-question support gets a proper visual hierarchy: each question past the first sits below a dashed divider, headed by a `Q1/N` tag + the original `q.header` chip. The `Q*/N` lets the user track progress without counting. - Inspector picks up the question count too: now shows "askUserQuestion · {first header} +N" when multiple are queued. Verified end-to-end on desktop with a CC-driven 2-question prompt (4-option + 3-option). Both selections feed back to CC as a single "User answers" payload, CC echoes both picks in its continuation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725) - Multi-question forms now use a top tab strip; single question renders inline. - Picking a single-select option auto-advances to the next unanswered question. - Drafts persist to tool message `pluginState.askUserDraft` so picks survive remount / HMR; new `setInterventionDraft` action on the chat store dispatches the pluginState patch. - Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for every unanswered question instead of letting the bridge time out into a cancelled isError — model gets a structured answer it can act on. - Visual: selected option now uses filled `colorPrimaryBg` + right-aligned check icon; index chip stays neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725) The async exit-handler cleanup raced Electron's main-process teardown and left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync unlink in the quit hook is the only reliable guarantee. Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q or `app.quit()`, not on external kills (test harness, OS shutdown). Verified by manual test: pending askUserQuestion forms now leave zero residue after both Cmd+Q and SIGTERM paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725) Submit now writes the structured `{ questionText: pickedLabel(s) }` payload to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so Render no longer has to scrape the bridge's prose `User answers:` content. Render shows one Q&A block per question — header + question + a checkmark card per picked option (multi-select fans out into multiple rows). Falls back to a `—` placeholder when answers are missing (older messages or skipped flows), and keeps the existing `pluginError` warning for cancel / no-answer paths. Also surfaces the answers in the Skill state inspector tab, which was previously empty for completed askUserQuestion messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725) Locks down the regression fixed in c0de0cd — async exit-handler cleanup losing to Electron's main-process teardown. Four cases: `before-quit` (Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown), `SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not throw on the second pass). `process.on` and `process.exit` are stubbed in the signal-path tests so the controller's listener attaches to a spy, not the test runner's process — otherwise we'd leak a real SIGTERM listener every test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
emaxlele
pushed a commit
to emaxlele/lobehub
that referenced
this pull request
May 11, 2026
…obehub#14639) * ✨ feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2) Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's built-in tool short-circuits in `-p` mode, so we host an in-process MCP server that exposes an equivalent `ask_user_question` tool. The handler blocks until the consumer submits an answer (or the 5min deadline / op shutdown fires), surfacing a structured `agent_intervention_request` / `agent_intervention_response` round-trip on the existing event stream. Added in this commit: - `packages/heterogeneous-agents/src/askUser/` - `AskUserBridge` — per-op pending map with timeout / cancel / progress keepalive support; emits an async-iterable of outbound events - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server, `?op=<id>` query routes via `AsyncLocalStorage` → `onsessioninitialized` → sessionId↔opId map; tool handler hands off to the matching bridge and pumps `notifications/progress` back to CC every 30s as wire-level keepalive (required for >5min waits, see spike notes) - `constants.ts` — shared tool/server names + the stable `apiName` the adapter rewrites to - Unit tests cover bridge lifecycle (resolve / cancel / timeout / progress / event stream) and an end-to-end MCP probe via `StreamableHTTPClientTransport` - `packages/agent-gateway-client/src/types.ts` — wire-level `agent_intervention_request` / `agent_intervention_response` event variants + payload interfaces. Re-exported through the package barrel. - `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter rewrites `apiName` to `askUserQuestion` so the renderer routes on a clean domain key. Identifier stays `claude-code`. Applied to both the main-agent and subagent paths for symmetry (subagent ask isn't expected today, but doesn't hurt). - `src/server/routers/lambda/aiAgent.ts` — Zod input schema for `aiAgent.heteroIngest` extended with the two new event types so the CLI sandbox can forward them through the server. No producer wiring yet — Steps 3-5 plug this into Electron main, the renderer executor, and the new UI. * ✨ feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3) Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now goes live during real prompts; renderer-submitted answers route back via new IPC. Changes - `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so controller-managed temp configs flow into the driver. - `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts` — append `--mcp-config <path>` when provided. Disallowed-tools pin stays so CC's built-in AskUserQuestion remains off (avoids double- registration of the same tool name). - `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts` - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt (de-duped concurrent first-callers via in-flight promise). - Per-op `setupInterventionForOp(opId, sessionId)`: registers an `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()` into the existing `heteroAgentEvent` broadcast. - Cleanup paths: exit handler `await intervention.cleanup()` settles pending MCP handlers + unlinks the temp config; pre-spawn errors short-circuit the same cleanup so we don't leak bridges on `buildSpawnPlan` / trace-session failures. - `before-quit` stops the MCP server (in addition to killing CC processes). - New `@IpcMethod() submitIntervention({ operationId, toolCallId, result?, cancelled?, cancelReason? })` — renderer side will dispatch answers / cancellations through this in Step 4/5. - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`. - `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy for `submitIntervention`. - New `claudeCode.test.ts` covers the four driver-arg paths (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay disallowed). Existing 28 controller tests still pass. What still doesn't run end-to-end - The renderer `heteroExecutor` doesn't consume `agent_intervention_request` yet — events go through the broadcast but the chat store ignores them. - No UI to render the intervention card or to call `submitIntervention`. Both lands in Steps 4/5 next. * ✨ feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4) Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId` from MCP `_meta`) instead of a random UUID, so the `agent_intervention_request` event references the same id as the existing tool message on the renderer side. Renderer-side `heteroExecutor` learns the new event: - Added `persistInterventionRequest(...)` next to `persistToolResult` — stamps `pluginState.askUserQuestion` (apiName + identifier + questions parsed from `arguments` + deadline + status='pending' + toolCallId) onto the matching tool message via `messageService.updateToolMessage`. - New branch in `handleStreamEvent` for `'agent_intervention_request'`: defers behind `persistQueue` (so it lands AFTER `persistToolBatch` populates `toolMsgIdByCallId`), then mirrors the same pluginState onto the in-memory message via `internal_dispatchMessage` so the UI lights up immediately — no fetchAndReplaceMessages round-trip needed. - The eventual `tool_result` for the same toolCallId hits the existing `tool_result` branch unchanged: it overwrites `pluginState` with whatever the result carries (typically undefined for our MCP tool, so `pluginState.askUserQuestion` clears and the intervention UI yields to the regular Render). Bridge tests cover the new contract: - caller-supplied toolCallId becomes the wire correlation key - duplicate-toolCallId pendings reject loudly so two-handler clobbers surface immediately 153 package tests + 1167 desktop main tests + 51 hetero executor tests still green; type-check clean. * ✨ feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5) Dedicated Render for the synthetic `askUserQuestion` apiName the adapter rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives under CC's render registry so the existing chat tool-detail flow picks it up automatically — no changes to the conversation framework. - New `AskUserQuestionItem` / `AskUserQuestionArgs` / `AskUserQuestionPluginState` types (mirrors CC's own AskUserQuestion schema verbatim). - `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'` member so the renders / inspectors / streamings registries can key off the same enum value. - `client/Render/AskUserQuestion/index.tsx` is the component: - `pluginState.askUserQuestion?.status === 'pending'` → renders the questions form (Select for single-select, CheckboxGroup for multi-select), a 5-min countdown ticking once a second, Submit / Skip buttons. Reads `operationId` via `messageOperationMap` so we can route through `heterogeneousAgentService.submitIntervention`. - Otherwise → renders the questions as muted captions plus the final answer text from `content`. Surfaces a warning when the tool_result was an error (timeout / cancelled / session ended). - Submit button stays disabled until every question has a selection; Skip always enabled (sends `cancelled: true`). - `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers the new component. What this does NOT do - Doesn't touch `BuiltinToolInterventions` — the form is rendered inside the regular tool body (Render slot), not the canonical intervention slot. Cleanest for now: the framework intervention flow assumes `submitToolInteraction` store actions, which would fight our IPC path. We can refactor onto that surface later if CC grows additional interactions (approval, file picker). - Doesn't translate strings — i18n in a follow-up. Type-check clean. Step 6 (real desktop e2e via CC) is next. * ✨ feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up) Step 5 registered the Render component but stopped at the registry — the chat tool-detail still returned the loading placeholder while `isToolCalling` was true, so users only ever saw a spinner during the 5 min intervention window. Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on CC + apiName=askUserQuestion tool messages) and route to the registered builtin Render inline before the placeholder branch. Once the intervention resolves, the eventual `tool_result` clears `pluginState.askUserQuestion` and the regular Render takes over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up) LOBE-8519 left two TODOs in `generationSlice` where hetero runtime silently fell through to client mode — regenerate would secretly hit the agent's underlying LLM, and continue would synthesize a fake "please continue" turn that confuses CC / Codex. - regenerateMessage: re-create the assistant row branched off the same user message, resolve resume sessionId (drop on cwd mismatch), then spawn a child `execHeterogeneousAgent` op so Stop only kills the executor, not the parent regenerate op. Mirrors sendMessage's hetero branch. - continueGenerationMessage: hetero CLIs have no continue primitive — each prompt is a fresh user turn — so bail out instead of polluting the session. - continueGenerationMessage: gateway mode now branches a server-side resume run instead of falling through to client. Surfaced while testing CC AskUserQuestion end-to-end on the LOBE-8725 branch (regenerating after an answered question went through the wrong runtime). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2 Two bugs surfaced when invoking the local-testing helper from a fresh session on macOS: - `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit code propagates through `pipefail`. With `set -e`, an empty pid set silently kills the whole script — `do_start` reported success, no Electron, no error. Trail with `|| true`. - `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`; process-tree teardown still works because `expand_descendants` walks the tree directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725) `AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across every CC subprocess. The SDK transport latches `_initialized=true` after the first `initialize`, so the second op's CC subprocess sees `Invalid Request: Server already initialized` (400) and reports the `lobe_cc` server as `failed`. From the model's POV the MCP tool is absent — it falls back to ToolSearch, can't find anything, and verbalizes the question instead. Refactor to the canonical multi-tenant pattern: one transport + one `McpServer` per session, looked up by the SDK-managed `mcp-session-id` header. New transports are minted on the first POST without a session id (must be an `initialize` request); subsequent requests route via the stored map; `onsessionclosed` cleans up. The first run of any process still works as before — this only matters once a second op spins up. Added a 3-op sequential regression test that fails on the old single-transport implementation and passes now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725) Step 5's first cut shoehorned the pending form into the Render slot and drove submit/skip with a custom `pluginState.askUserQuestion.status` field, which forced three layers of glue: - `Tool/Detail` had to bypass the loading placeholder via an identifier+apiName hardcode so the form would surface during `isToolCalling` - The executor had to `messageService.getMessages → replaceMessages` after `agent_intervention_request` to drag the freshly-created tool row into in-memory state (the framework's own `tool_end → fetchAndReplaceMessages` only fires after the user answers) - The executor also had to `associateMessageWithOperation` for the tool row so the form could look up the running CC op for IPC All three were patches around skipping the canonical surface. This commit moves AskUserQuestion onto `pluginIntervention.status='pending'` and the `BuiltinToolInterventions` registry, which the framework already drives end-to-end: - `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx` — pure form, no IPC, no store reads. Resolves through the standard `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback. - `Render/AskUserQuestion` shrinks to the answered/aborted view only; the framework hides Render while pending, so no status switching. - New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}" chip in the inline tool body, matching the rest of CC's tools. - Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`; `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry. Hetero needs a different action handler than `submitToolInteraction` (which spawns `executeClientAgent` — wrong for a CC subprocess that's already blocked on an MCP call). Two thin pieces wire that: - `submitHeteroIntervention` (chat store) — sets `pluginIntervention` via `optimisticUpdateMessagePlugin` (which already syncs DB + in-memory + parent-assistant `tools[].intervention` in one shot), then forwards the answer through `heterogeneousAgentService.submitIntervention` IPC. Operation lookup walks the tool message's `parentId` to hit the assistant's `messageOperationMap` entry — drops the explicit `associateMessageWithOperation` call from the executor. - `customInteractionHandlers.isHeteroInteractionIdentifier` flags `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits there before reaching the existing `submitToolInteraction` path. Executor change collapses to one line: `optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`. The post-intervention refresh, the associate call, and the `persistInterventionRequest` helper all go away. Removed: - `AskUserQuestionPluginState` type (custom field is gone) - `Tool/Detail` `askUserPending` inline-render branch - Executor `messageService.getMessages + replaceMessages` round-trip - Executor `associateMessageWithOperation` for tool rows - `persistInterventionRequest` helper Verified end-to-end against a real CC subprocess on desktop: - Inline body shows the new Inspector chip; pending form lives in the bottom InterventionBar (canonical surface) - Submit ships answer through MCP, CC continues with structured result - Skip flips status to `rejected`, framework's RejectedResponse shows "User skipped"; CC receives isError and falls back to text - `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op (the per-session transport fix from the previous commit) - `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725) Select dropdown was the wrong primitive — it hides options behind an extra click and doesn't read like a question to answer. CC's underlying tool is 1-4 questions × 2-4 options, so the whole option set always fits inline. - Each option renders as a clickable card: numbered chip (1/2/3/4) + bold label + secondary description on a single row. Hover tints the background; selected state lights up `colorPrimary` on both the chip and the card outline so the pick is unmistakable at a glance. - Multi-select (`q.multiSelect`) toggles instead of replacing, with a "(multi-select)" hint in the question header. - Multi-question support gets a proper visual hierarchy: each question past the first sits below a dashed divider, headed by a `Q1/N` tag + the original `q.header` chip. The `Q*/N` lets the user track progress without counting. - Inspector picks up the question count too: now shows "askUserQuestion · {first header} +N" when multiple are queued. Verified end-to-end on desktop with a CC-driven 2-question prompt (4-option + 3-option). Both selections feed back to CC as a single "User answers" payload, CC echoes both picks in its continuation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725) - Multi-question forms now use a top tab strip; single question renders inline. - Picking a single-select option auto-advances to the next unanswered question. - Drafts persist to tool message `pluginState.askUserDraft` so picks survive remount / HMR; new `setInterventionDraft` action on the chat store dispatches the pluginState patch. - Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for every unanswered question instead of letting the bridge time out into a cancelled isError — model gets a structured answer it can act on. - Visual: selected option now uses filled `colorPrimaryBg` + right-aligned check icon; index chip stays neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725) The async exit-handler cleanup raced Electron's main-process teardown and left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync unlink in the quit hook is the only reliable guarantee. Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q or `app.quit()`, not on external kills (test harness, OS shutdown). Verified by manual test: pending askUserQuestion forms now leave zero residue after both Cmd+Q and SIGTERM paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725) Submit now writes the structured `{ questionText: pickedLabel(s) }` payload to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so Render no longer has to scrape the bridge's prose `User answers:` content. Render shows one Q&A block per question — header + question + a checkmark card per picked option (multi-select fans out into multiple rows). Falls back to a `—` placeholder when answers are missing (older messages or skipped flows), and keeps the existing `pluginError` warning for cancel / no-answer paths. Also surfaces the answers in the Skill state inspector tab, which was previously empty for completed askUserQuestion messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725) Locks down the regression fixed in c0de0cd — async exit-handler cleanup losing to Electron's main-process teardown. Four cases: `before-quit` (Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown), `SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not throw on the second pass). `process.on` and `process.exit` are stubbed in the signal-path tests so the controller's listener attaches to a spy, not the test runner's process — otherwise we'd leak a real SIGTERM listener every test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
emaxlele
pushed a commit
to emaxlele/lobehub
that referenced
this pull request
May 11, 2026
…obehub#14639) * ✨ feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2) Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's built-in tool short-circuits in `-p` mode, so we host an in-process MCP server that exposes an equivalent `ask_user_question` tool. The handler blocks until the consumer submits an answer (or the 5min deadline / op shutdown fires), surfacing a structured `agent_intervention_request` / `agent_intervention_response` round-trip on the existing event stream. Added in this commit: - `packages/heterogeneous-agents/src/askUser/` - `AskUserBridge` — per-op pending map with timeout / cancel / progress keepalive support; emits an async-iterable of outbound events - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server, `?op=<id>` query routes via `AsyncLocalStorage` → `onsessioninitialized` → sessionId↔opId map; tool handler hands off to the matching bridge and pumps `notifications/progress` back to CC every 30s as wire-level keepalive (required for >5min waits, see spike notes) - `constants.ts` — shared tool/server names + the stable `apiName` the adapter rewrites to - Unit tests cover bridge lifecycle (resolve / cancel / timeout / progress / event stream) and an end-to-end MCP probe via `StreamableHTTPClientTransport` - `packages/agent-gateway-client/src/types.ts` — wire-level `agent_intervention_request` / `agent_intervention_response` event variants + payload interfaces. Re-exported through the package barrel. - `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter rewrites `apiName` to `askUserQuestion` so the renderer routes on a clean domain key. Identifier stays `claude-code`. Applied to both the main-agent and subagent paths for symmetry (subagent ask isn't expected today, but doesn't hurt). - `src/server/routers/lambda/aiAgent.ts` — Zod input schema for `aiAgent.heteroIngest` extended with the two new event types so the CLI sandbox can forward them through the server. No producer wiring yet — Steps 3-5 plug this into Electron main, the renderer executor, and the new UI. * ✨ feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3) Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now goes live during real prompts; renderer-submitted answers route back via new IPC. Changes - `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so controller-managed temp configs flow into the driver. - `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts` — append `--mcp-config <path>` when provided. Disallowed-tools pin stays so CC's built-in AskUserQuestion remains off (avoids double- registration of the same tool name). - `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts` - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt (de-duped concurrent first-callers via in-flight promise). - Per-op `setupInterventionForOp(opId, sessionId)`: registers an `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()` into the existing `heteroAgentEvent` broadcast. - Cleanup paths: exit handler `await intervention.cleanup()` settles pending MCP handlers + unlinks the temp config; pre-spawn errors short-circuit the same cleanup so we don't leak bridges on `buildSpawnPlan` / trace-session failures. - `before-quit` stops the MCP server (in addition to killing CC processes). - New `@IpcMethod() submitIntervention({ operationId, toolCallId, result?, cancelled?, cancelReason? })` — renderer side will dispatch answers / cancellations through this in Step 4/5. - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`. - `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy for `submitIntervention`. - New `claudeCode.test.ts` covers the four driver-arg paths (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay disallowed). Existing 28 controller tests still pass. What still doesn't run end-to-end - The renderer `heteroExecutor` doesn't consume `agent_intervention_request` yet — events go through the broadcast but the chat store ignores them. - No UI to render the intervention card or to call `submitIntervention`. Both lands in Steps 4/5 next. * ✨ feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4) Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId` from MCP `_meta`) instead of a random UUID, so the `agent_intervention_request` event references the same id as the existing tool message on the renderer side. Renderer-side `heteroExecutor` learns the new event: - Added `persistInterventionRequest(...)` next to `persistToolResult` — stamps `pluginState.askUserQuestion` (apiName + identifier + questions parsed from `arguments` + deadline + status='pending' + toolCallId) onto the matching tool message via `messageService.updateToolMessage`. - New branch in `handleStreamEvent` for `'agent_intervention_request'`: defers behind `persistQueue` (so it lands AFTER `persistToolBatch` populates `toolMsgIdByCallId`), then mirrors the same pluginState onto the in-memory message via `internal_dispatchMessage` so the UI lights up immediately — no fetchAndReplaceMessages round-trip needed. - The eventual `tool_result` for the same toolCallId hits the existing `tool_result` branch unchanged: it overwrites `pluginState` with whatever the result carries (typically undefined for our MCP tool, so `pluginState.askUserQuestion` clears and the intervention UI yields to the regular Render). Bridge tests cover the new contract: - caller-supplied toolCallId becomes the wire correlation key - duplicate-toolCallId pendings reject loudly so two-handler clobbers surface immediately 153 package tests + 1167 desktop main tests + 51 hetero executor tests still green; type-check clean. * ✨ feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5) Dedicated Render for the synthetic `askUserQuestion` apiName the adapter rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives under CC's render registry so the existing chat tool-detail flow picks it up automatically — no changes to the conversation framework. - New `AskUserQuestionItem` / `AskUserQuestionArgs` / `AskUserQuestionPluginState` types (mirrors CC's own AskUserQuestion schema verbatim). - `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'` member so the renders / inspectors / streamings registries can key off the same enum value. - `client/Render/AskUserQuestion/index.tsx` is the component: - `pluginState.askUserQuestion?.status === 'pending'` → renders the questions form (Select for single-select, CheckboxGroup for multi-select), a 5-min countdown ticking once a second, Submit / Skip buttons. Reads `operationId` via `messageOperationMap` so we can route through `heterogeneousAgentService.submitIntervention`. - Otherwise → renders the questions as muted captions plus the final answer text from `content`. Surfaces a warning when the tool_result was an error (timeout / cancelled / session ended). - Submit button stays disabled until every question has a selection; Skip always enabled (sends `cancelled: true`). - `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers the new component. What this does NOT do - Doesn't touch `BuiltinToolInterventions` — the form is rendered inside the regular tool body (Render slot), not the canonical intervention slot. Cleanest for now: the framework intervention flow assumes `submitToolInteraction` store actions, which would fight our IPC path. We can refactor onto that surface later if CC grows additional interactions (approval, file picker). - Doesn't translate strings — i18n in a follow-up. Type-check clean. Step 6 (real desktop e2e via CC) is next. * ✨ feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up) Step 5 registered the Render component but stopped at the registry — the chat tool-detail still returned the loading placeholder while `isToolCalling` was true, so users only ever saw a spinner during the 5 min intervention window. Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on CC + apiName=askUserQuestion tool messages) and route to the registered builtin Render inline before the placeholder branch. Once the intervention resolves, the eventual `tool_result` clears `pluginState.askUserQuestion` and the regular Render takes over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up) LOBE-8519 left two TODOs in `generationSlice` where hetero runtime silently fell through to client mode — regenerate would secretly hit the agent's underlying LLM, and continue would synthesize a fake "please continue" turn that confuses CC / Codex. - regenerateMessage: re-create the assistant row branched off the same user message, resolve resume sessionId (drop on cwd mismatch), then spawn a child `execHeterogeneousAgent` op so Stop only kills the executor, not the parent regenerate op. Mirrors sendMessage's hetero branch. - continueGenerationMessage: hetero CLIs have no continue primitive — each prompt is a fresh user turn — so bail out instead of polluting the session. - continueGenerationMessage: gateway mode now branches a server-side resume run instead of falling through to client. Surfaced while testing CC AskUserQuestion end-to-end on the LOBE-8725 branch (regenerating after an answered question went through the wrong runtime). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2 Two bugs surfaced when invoking the local-testing helper from a fresh session on macOS: - `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit code propagates through `pipefail`. With `set -e`, an empty pid set silently kills the whole script — `do_start` reported success, no Electron, no error. Trail with `|| true`. - `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`; process-tree teardown still works because `expand_descendants` walks the tree directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725) `AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across every CC subprocess. The SDK transport latches `_initialized=true` after the first `initialize`, so the second op's CC subprocess sees `Invalid Request: Server already initialized` (400) and reports the `lobe_cc` server as `failed`. From the model's POV the MCP tool is absent — it falls back to ToolSearch, can't find anything, and verbalizes the question instead. Refactor to the canonical multi-tenant pattern: one transport + one `McpServer` per session, looked up by the SDK-managed `mcp-session-id` header. New transports are minted on the first POST without a session id (must be an `initialize` request); subsequent requests route via the stored map; `onsessionclosed` cleans up. The first run of any process still works as before — this only matters once a second op spins up. Added a 3-op sequential regression test that fails on the old single-transport implementation and passes now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725) Step 5's first cut shoehorned the pending form into the Render slot and drove submit/skip with a custom `pluginState.askUserQuestion.status` field, which forced three layers of glue: - `Tool/Detail` had to bypass the loading placeholder via an identifier+apiName hardcode so the form would surface during `isToolCalling` - The executor had to `messageService.getMessages → replaceMessages` after `agent_intervention_request` to drag the freshly-created tool row into in-memory state (the framework's own `tool_end → fetchAndReplaceMessages` only fires after the user answers) - The executor also had to `associateMessageWithOperation` for the tool row so the form could look up the running CC op for IPC All three were patches around skipping the canonical surface. This commit moves AskUserQuestion onto `pluginIntervention.status='pending'` and the `BuiltinToolInterventions` registry, which the framework already drives end-to-end: - `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx` — pure form, no IPC, no store reads. Resolves through the standard `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback. - `Render/AskUserQuestion` shrinks to the answered/aborted view only; the framework hides Render while pending, so no status switching. - New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}" chip in the inline tool body, matching the rest of CC's tools. - Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`; `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry. Hetero needs a different action handler than `submitToolInteraction` (which spawns `executeClientAgent` — wrong for a CC subprocess that's already blocked on an MCP call). Two thin pieces wire that: - `submitHeteroIntervention` (chat store) — sets `pluginIntervention` via `optimisticUpdateMessagePlugin` (which already syncs DB + in-memory + parent-assistant `tools[].intervention` in one shot), then forwards the answer through `heterogeneousAgentService.submitIntervention` IPC. Operation lookup walks the tool message's `parentId` to hit the assistant's `messageOperationMap` entry — drops the explicit `associateMessageWithOperation` call from the executor. - `customInteractionHandlers.isHeteroInteractionIdentifier` flags `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits there before reaching the existing `submitToolInteraction` path. Executor change collapses to one line: `optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`. The post-intervention refresh, the associate call, and the `persistInterventionRequest` helper all go away. Removed: - `AskUserQuestionPluginState` type (custom field is gone) - `Tool/Detail` `askUserPending` inline-render branch - Executor `messageService.getMessages + replaceMessages` round-trip - Executor `associateMessageWithOperation` for tool rows - `persistInterventionRequest` helper Verified end-to-end against a real CC subprocess on desktop: - Inline body shows the new Inspector chip; pending form lives in the bottom InterventionBar (canonical surface) - Submit ships answer through MCP, CC continues with structured result - Skip flips status to `rejected`, framework's RejectedResponse shows "User skipped"; CC receives isError and falls back to text - `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op (the per-session transport fix from the previous commit) - `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725) Select dropdown was the wrong primitive — it hides options behind an extra click and doesn't read like a question to answer. CC's underlying tool is 1-4 questions × 2-4 options, so the whole option set always fits inline. - Each option renders as a clickable card: numbered chip (1/2/3/4) + bold label + secondary description on a single row. Hover tints the background; selected state lights up `colorPrimary` on both the chip and the card outline so the pick is unmistakable at a glance. - Multi-select (`q.multiSelect`) toggles instead of replacing, with a "(multi-select)" hint in the question header. - Multi-question support gets a proper visual hierarchy: each question past the first sits below a dashed divider, headed by a `Q1/N` tag + the original `q.header` chip. The `Q*/N` lets the user track progress without counting. - Inspector picks up the question count too: now shows "askUserQuestion · {first header} +N" when multiple are queued. Verified end-to-end on desktop with a CC-driven 2-question prompt (4-option + 3-option). Both selections feed back to CC as a single "User answers" payload, CC echoes both picks in its continuation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725) - Multi-question forms now use a top tab strip; single question renders inline. - Picking a single-select option auto-advances to the next unanswered question. - Drafts persist to tool message `pluginState.askUserDraft` so picks survive remount / HMR; new `setInterventionDraft` action on the chat store dispatches the pluginState patch. - Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for every unanswered question instead of letting the bridge time out into a cancelled isError — model gets a structured answer it can act on. - Visual: selected option now uses filled `colorPrimaryBg` + right-aligned check icon; index chip stays neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725) The async exit-handler cleanup raced Electron's main-process teardown and left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync unlink in the quit hook is the only reliable guarantee. Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q or `app.quit()`, not on external kills (test harness, OS shutdown). Verified by manual test: pending askUserQuestion forms now leave zero residue after both Cmd+Q and SIGTERM paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725) Submit now writes the structured `{ questionText: pickedLabel(s) }` payload to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so Render no longer has to scrape the bridge's prose `User answers:` content. Render shows one Q&A block per question — header + question + a checkmark card per picked option (multi-select fans out into multiple rows). Falls back to a `—` placeholder when answers are missing (older messages or skipped flows), and keeps the existing `pluginError` warning for cancel / no-answer paths. Also surfaces the answers in the Skill state inspector tab, which was previously empty for completed askUserQuestion messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725) Locks down the regression fixed in c0de0cd — async exit-handler cleanup losing to Electron's main-process teardown. Four cases: `before-quit` (Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown), `SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not throw on the second pass). `process.on` and `process.exit` are stubbed in the signal-path tests so the controller's listener attaches to a spy, not the test runner's process — otherwise we'd leak a real SIGTERM listener every test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closed
Coooolfan
pushed a commit
to Coooolfan/lobehub
that referenced
this pull request
May 12, 2026
…obehub#14639) * ✨ feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2) Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's built-in tool short-circuits in `-p` mode, so we host an in-process MCP server that exposes an equivalent `ask_user_question` tool. The handler blocks until the consumer submits an answer (or the 5min deadline / op shutdown fires), surfacing a structured `agent_intervention_request` / `agent_intervention_response` round-trip on the existing event stream. Added in this commit: - `packages/heterogeneous-agents/src/askUser/` - `AskUserBridge` — per-op pending map with timeout / cancel / progress keepalive support; emits an async-iterable of outbound events - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server, `?op=<id>` query routes via `AsyncLocalStorage` → `onsessioninitialized` → sessionId↔opId map; tool handler hands off to the matching bridge and pumps `notifications/progress` back to CC every 30s as wire-level keepalive (required for >5min waits, see spike notes) - `constants.ts` — shared tool/server names + the stable `apiName` the adapter rewrites to - Unit tests cover bridge lifecycle (resolve / cancel / timeout / progress / event stream) and an end-to-end MCP probe via `StreamableHTTPClientTransport` - `packages/agent-gateway-client/src/types.ts` — wire-level `agent_intervention_request` / `agent_intervention_response` event variants + payload interfaces. Re-exported through the package barrel. - `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter rewrites `apiName` to `askUserQuestion` so the renderer routes on a clean domain key. Identifier stays `claude-code`. Applied to both the main-agent and subagent paths for symmetry (subagent ask isn't expected today, but doesn't hurt). - `src/server/routers/lambda/aiAgent.ts` — Zod input schema for `aiAgent.heteroIngest` extended with the two new event types so the CLI sandbox can forward them through the server. No producer wiring yet — Steps 3-5 plug this into Electron main, the renderer executor, and the new UI. * ✨ feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3) Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now goes live during real prompts; renderer-submitted answers route back via new IPC. Changes - `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so controller-managed temp configs flow into the driver. - `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts` — append `--mcp-config <path>` when provided. Disallowed-tools pin stays so CC's built-in AskUserQuestion remains off (avoids double- registration of the same tool name). - `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts` - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt (de-duped concurrent first-callers via in-flight promise). - Per-op `setupInterventionForOp(opId, sessionId)`: registers an `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()` into the existing `heteroAgentEvent` broadcast. - Cleanup paths: exit handler `await intervention.cleanup()` settles pending MCP handlers + unlinks the temp config; pre-spawn errors short-circuit the same cleanup so we don't leak bridges on `buildSpawnPlan` / trace-session failures. - `before-quit` stops the MCP server (in addition to killing CC processes). - New `@IpcMethod() submitIntervention({ operationId, toolCallId, result?, cancelled?, cancelReason? })` — renderer side will dispatch answers / cancellations through this in Step 4/5. - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`. - `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy for `submitIntervention`. - New `claudeCode.test.ts` covers the four driver-arg paths (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay disallowed). Existing 28 controller tests still pass. What still doesn't run end-to-end - The renderer `heteroExecutor` doesn't consume `agent_intervention_request` yet — events go through the broadcast but the chat store ignores them. - No UI to render the intervention card or to call `submitIntervention`. Both lands in Steps 4/5 next. * ✨ feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4) Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId` from MCP `_meta`) instead of a random UUID, so the `agent_intervention_request` event references the same id as the existing tool message on the renderer side. Renderer-side `heteroExecutor` learns the new event: - Added `persistInterventionRequest(...)` next to `persistToolResult` — stamps `pluginState.askUserQuestion` (apiName + identifier + questions parsed from `arguments` + deadline + status='pending' + toolCallId) onto the matching tool message via `messageService.updateToolMessage`. - New branch in `handleStreamEvent` for `'agent_intervention_request'`: defers behind `persistQueue` (so it lands AFTER `persistToolBatch` populates `toolMsgIdByCallId`), then mirrors the same pluginState onto the in-memory message via `internal_dispatchMessage` so the UI lights up immediately — no fetchAndReplaceMessages round-trip needed. - The eventual `tool_result` for the same toolCallId hits the existing `tool_result` branch unchanged: it overwrites `pluginState` with whatever the result carries (typically undefined for our MCP tool, so `pluginState.askUserQuestion` clears and the intervention UI yields to the regular Render). Bridge tests cover the new contract: - caller-supplied toolCallId becomes the wire correlation key - duplicate-toolCallId pendings reject loudly so two-handler clobbers surface immediately 153 package tests + 1167 desktop main tests + 51 hetero executor tests still green; type-check clean. * ✨ feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5) Dedicated Render for the synthetic `askUserQuestion` apiName the adapter rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives under CC's render registry so the existing chat tool-detail flow picks it up automatically — no changes to the conversation framework. - New `AskUserQuestionItem` / `AskUserQuestionArgs` / `AskUserQuestionPluginState` types (mirrors CC's own AskUserQuestion schema verbatim). - `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'` member so the renders / inspectors / streamings registries can key off the same enum value. - `client/Render/AskUserQuestion/index.tsx` is the component: - `pluginState.askUserQuestion?.status === 'pending'` → renders the questions form (Select for single-select, CheckboxGroup for multi-select), a 5-min countdown ticking once a second, Submit / Skip buttons. Reads `operationId` via `messageOperationMap` so we can route through `heterogeneousAgentService.submitIntervention`. - Otherwise → renders the questions as muted captions plus the final answer text from `content`. Surfaces a warning when the tool_result was an error (timeout / cancelled / session ended). - Submit button stays disabled until every question has a selection; Skip always enabled (sends `cancelled: true`). - `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers the new component. What this does NOT do - Doesn't touch `BuiltinToolInterventions` — the form is rendered inside the regular tool body (Render slot), not the canonical intervention slot. Cleanest for now: the framework intervention flow assumes `submitToolInteraction` store actions, which would fight our IPC path. We can refactor onto that surface later if CC grows additional interactions (approval, file picker). - Doesn't translate strings — i18n in a follow-up. Type-check clean. Step 6 (real desktop e2e via CC) is next. * ✨ feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up) Step 5 registered the Render component but stopped at the registry — the chat tool-detail still returned the loading placeholder while `isToolCalling` was true, so users only ever saw a spinner during the 5 min intervention window. Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on CC + apiName=askUserQuestion tool messages) and route to the registered builtin Render inline before the placeholder branch. Once the intervention resolves, the eventual `tool_result` clears `pluginState.askUserQuestion` and the regular Render takes over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up) LOBE-8519 left two TODOs in `generationSlice` where hetero runtime silently fell through to client mode — regenerate would secretly hit the agent's underlying LLM, and continue would synthesize a fake "please continue" turn that confuses CC / Codex. - regenerateMessage: re-create the assistant row branched off the same user message, resolve resume sessionId (drop on cwd mismatch), then spawn a child `execHeterogeneousAgent` op so Stop only kills the executor, not the parent regenerate op. Mirrors sendMessage's hetero branch. - continueGenerationMessage: hetero CLIs have no continue primitive — each prompt is a fresh user turn — so bail out instead of polluting the session. - continueGenerationMessage: gateway mode now branches a server-side resume run instead of falling through to client. Surfaced while testing CC AskUserQuestion end-to-end on the LOBE-8725 branch (regenerating after an answered question went through the wrong runtime). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2 Two bugs surfaced when invoking the local-testing helper from a fresh session on macOS: - `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit code propagates through `pipefail`. With `set -e`, an empty pid set silently kills the whole script — `do_start` reported success, no Electron, no error. Trail with `|| true`. - `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`; process-tree teardown still works because `expand_descendants` walks the tree directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725) `AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across every CC subprocess. The SDK transport latches `_initialized=true` after the first `initialize`, so the second op's CC subprocess sees `Invalid Request: Server already initialized` (400) and reports the `lobe_cc` server as `failed`. From the model's POV the MCP tool is absent — it falls back to ToolSearch, can't find anything, and verbalizes the question instead. Refactor to the canonical multi-tenant pattern: one transport + one `McpServer` per session, looked up by the SDK-managed `mcp-session-id` header. New transports are minted on the first POST without a session id (must be an `initialize` request); subsequent requests route via the stored map; `onsessionclosed` cleans up. The first run of any process still works as before — this only matters once a second op spins up. Added a 3-op sequential regression test that fails on the old single-transport implementation and passes now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725) Step 5's first cut shoehorned the pending form into the Render slot and drove submit/skip with a custom `pluginState.askUserQuestion.status` field, which forced three layers of glue: - `Tool/Detail` had to bypass the loading placeholder via an identifier+apiName hardcode so the form would surface during `isToolCalling` - The executor had to `messageService.getMessages → replaceMessages` after `agent_intervention_request` to drag the freshly-created tool row into in-memory state (the framework's own `tool_end → fetchAndReplaceMessages` only fires after the user answers) - The executor also had to `associateMessageWithOperation` for the tool row so the form could look up the running CC op for IPC All three were patches around skipping the canonical surface. This commit moves AskUserQuestion onto `pluginIntervention.status='pending'` and the `BuiltinToolInterventions` registry, which the framework already drives end-to-end: - `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx` — pure form, no IPC, no store reads. Resolves through the standard `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback. - `Render/AskUserQuestion` shrinks to the answered/aborted view only; the framework hides Render while pending, so no status switching. - New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}" chip in the inline tool body, matching the rest of CC's tools. - Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`; `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry. Hetero needs a different action handler than `submitToolInteraction` (which spawns `executeClientAgent` — wrong for a CC subprocess that's already blocked on an MCP call). Two thin pieces wire that: - `submitHeteroIntervention` (chat store) — sets `pluginIntervention` via `optimisticUpdateMessagePlugin` (which already syncs DB + in-memory + parent-assistant `tools[].intervention` in one shot), then forwards the answer through `heterogeneousAgentService.submitIntervention` IPC. Operation lookup walks the tool message's `parentId` to hit the assistant's `messageOperationMap` entry — drops the explicit `associateMessageWithOperation` call from the executor. - `customInteractionHandlers.isHeteroInteractionIdentifier` flags `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits there before reaching the existing `submitToolInteraction` path. Executor change collapses to one line: `optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`. The post-intervention refresh, the associate call, and the `persistInterventionRequest` helper all go away. Removed: - `AskUserQuestionPluginState` type (custom field is gone) - `Tool/Detail` `askUserPending` inline-render branch - Executor `messageService.getMessages + replaceMessages` round-trip - Executor `associateMessageWithOperation` for tool rows - `persistInterventionRequest` helper Verified end-to-end against a real CC subprocess on desktop: - Inline body shows the new Inspector chip; pending form lives in the bottom InterventionBar (canonical surface) - Submit ships answer through MCP, CC continues with structured result - Skip flips status to `rejected`, framework's RejectedResponse shows "User skipped"; CC receives isError and falls back to text - `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op (the per-session transport fix from the previous commit) - `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725) Select dropdown was the wrong primitive — it hides options behind an extra click and doesn't read like a question to answer. CC's underlying tool is 1-4 questions × 2-4 options, so the whole option set always fits inline. - Each option renders as a clickable card: numbered chip (1/2/3/4) + bold label + secondary description on a single row. Hover tints the background; selected state lights up `colorPrimary` on both the chip and the card outline so the pick is unmistakable at a glance. - Multi-select (`q.multiSelect`) toggles instead of replacing, with a "(multi-select)" hint in the question header. - Multi-question support gets a proper visual hierarchy: each question past the first sits below a dashed divider, headed by a `Q1/N` tag + the original `q.header` chip. The `Q*/N` lets the user track progress without counting. - Inspector picks up the question count too: now shows "askUserQuestion · {first header} +N" when multiple are queued. Verified end-to-end on desktop with a CC-driven 2-question prompt (4-option + 3-option). Both selections feed back to CC as a single "User answers" payload, CC echoes both picks in its continuation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725) - Multi-question forms now use a top tab strip; single question renders inline. - Picking a single-select option auto-advances to the next unanswered question. - Drafts persist to tool message `pluginState.askUserDraft` so picks survive remount / HMR; new `setInterventionDraft` action on the chat store dispatches the pluginState patch. - Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for every unanswered question instead of letting the bridge time out into a cancelled isError — model gets a structured answer it can act on. - Visual: selected option now uses filled `colorPrimaryBg` + right-aligned check icon; index chip stays neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725) The async exit-handler cleanup raced Electron's main-process teardown and left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync unlink in the quit hook is the only reliable guarantee. Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q or `app.quit()`, not on external kills (test harness, OS shutdown). Verified by manual test: pending askUserQuestion forms now leave zero residue after both Cmd+Q and SIGTERM paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725) Submit now writes the structured `{ questionText: pickedLabel(s) }` payload to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so Render no longer has to scrape the bridge's prose `User answers:` content. Render shows one Q&A block per question — header + question + a checkmark card per picked option (multi-select fans out into multiple rows). Falls back to a `—` placeholder when answers are missing (older messages or skipped flows), and keeps the existing `pluginError` warning for cancel / no-answer paths. Also surfaces the answers in the Skill state inspector tab, which was previously empty for completed askUserQuestion messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725) Locks down the regression fixed in c0de0cd — async exit-handler cleanup losing to Electron's main-process teardown. Four cases: `before-quit` (Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown), `SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not throw on the second pass). `process.on` and `process.exit` are stubbed in the signal-path tests so the controller's listener attaches to a spy, not the test runner's process — otherwise we'd leak a real SIGTERM listener every test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Innei
pushed a commit
to Innei/lobehub
that referenced
this pull request
May 12, 2026
…obehub#14639) * ✨ feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2) Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's built-in tool short-circuits in `-p` mode, so we host an in-process MCP server that exposes an equivalent `ask_user_question` tool. The handler blocks until the consumer submits an answer (or the 5min deadline / op shutdown fires), surfacing a structured `agent_intervention_request` / `agent_intervention_response` round-trip on the existing event stream. Added in this commit: - `packages/heterogeneous-agents/src/askUser/` - `AskUserBridge` — per-op pending map with timeout / cancel / progress keepalive support; emits an async-iterable of outbound events - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server, `?op=<id>` query routes via `AsyncLocalStorage` → `onsessioninitialized` → sessionId↔opId map; tool handler hands off to the matching bridge and pumps `notifications/progress` back to CC every 30s as wire-level keepalive (required for >5min waits, see spike notes) - `constants.ts` — shared tool/server names + the stable `apiName` the adapter rewrites to - Unit tests cover bridge lifecycle (resolve / cancel / timeout / progress / event stream) and an end-to-end MCP probe via `StreamableHTTPClientTransport` - `packages/agent-gateway-client/src/types.ts` — wire-level `agent_intervention_request` / `agent_intervention_response` event variants + payload interfaces. Re-exported through the package barrel. - `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter rewrites `apiName` to `askUserQuestion` so the renderer routes on a clean domain key. Identifier stays `claude-code`. Applied to both the main-agent and subagent paths for symmetry (subagent ask isn't expected today, but doesn't hurt). - `src/server/routers/lambda/aiAgent.ts` — Zod input schema for `aiAgent.heteroIngest` extended with the two new event types so the CLI sandbox can forward them through the server. No producer wiring yet — Steps 3-5 plug this into Electron main, the renderer executor, and the new UI. * ✨ feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3) Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now goes live during real prompts; renderer-submitted answers route back via new IPC. Changes - `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so controller-managed temp configs flow into the driver. - `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts` — append `--mcp-config <path>` when provided. Disallowed-tools pin stays so CC's built-in AskUserQuestion remains off (avoids double- registration of the same tool name). - `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts` - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt (de-duped concurrent first-callers via in-flight promise). - Per-op `setupInterventionForOp(opId, sessionId)`: registers an `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()` into the existing `heteroAgentEvent` broadcast. - Cleanup paths: exit handler `await intervention.cleanup()` settles pending MCP handlers + unlinks the temp config; pre-spawn errors short-circuit the same cleanup so we don't leak bridges on `buildSpawnPlan` / trace-session failures. - `before-quit` stops the MCP server (in addition to killing CC processes). - New `@IpcMethod() submitIntervention({ operationId, toolCallId, result?, cancelled?, cancelReason? })` — renderer side will dispatch answers / cancellations through this in Step 4/5. - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`. - `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy for `submitIntervention`. - New `claudeCode.test.ts` covers the four driver-arg paths (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay disallowed). Existing 28 controller tests still pass. What still doesn't run end-to-end - The renderer `heteroExecutor` doesn't consume `agent_intervention_request` yet — events go through the broadcast but the chat store ignores them. - No UI to render the intervention card or to call `submitIntervention`. Both lands in Steps 4/5 next. * ✨ feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4) Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId` from MCP `_meta`) instead of a random UUID, so the `agent_intervention_request` event references the same id as the existing tool message on the renderer side. Renderer-side `heteroExecutor` learns the new event: - Added `persistInterventionRequest(...)` next to `persistToolResult` — stamps `pluginState.askUserQuestion` (apiName + identifier + questions parsed from `arguments` + deadline + status='pending' + toolCallId) onto the matching tool message via `messageService.updateToolMessage`. - New branch in `handleStreamEvent` for `'agent_intervention_request'`: defers behind `persistQueue` (so it lands AFTER `persistToolBatch` populates `toolMsgIdByCallId`), then mirrors the same pluginState onto the in-memory message via `internal_dispatchMessage` so the UI lights up immediately — no fetchAndReplaceMessages round-trip needed. - The eventual `tool_result` for the same toolCallId hits the existing `tool_result` branch unchanged: it overwrites `pluginState` with whatever the result carries (typically undefined for our MCP tool, so `pluginState.askUserQuestion` clears and the intervention UI yields to the regular Render). Bridge tests cover the new contract: - caller-supplied toolCallId becomes the wire correlation key - duplicate-toolCallId pendings reject loudly so two-handler clobbers surface immediately 153 package tests + 1167 desktop main tests + 51 hetero executor tests still green; type-check clean. * ✨ feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5) Dedicated Render for the synthetic `askUserQuestion` apiName the adapter rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives under CC's render registry so the existing chat tool-detail flow picks it up automatically — no changes to the conversation framework. - New `AskUserQuestionItem` / `AskUserQuestionArgs` / `AskUserQuestionPluginState` types (mirrors CC's own AskUserQuestion schema verbatim). - `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'` member so the renders / inspectors / streamings registries can key off the same enum value. - `client/Render/AskUserQuestion/index.tsx` is the component: - `pluginState.askUserQuestion?.status === 'pending'` → renders the questions form (Select for single-select, CheckboxGroup for multi-select), a 5-min countdown ticking once a second, Submit / Skip buttons. Reads `operationId` via `messageOperationMap` so we can route through `heterogeneousAgentService.submitIntervention`. - Otherwise → renders the questions as muted captions plus the final answer text from `content`. Surfaces a warning when the tool_result was an error (timeout / cancelled / session ended). - Submit button stays disabled until every question has a selection; Skip always enabled (sends `cancelled: true`). - `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers the new component. What this does NOT do - Doesn't touch `BuiltinToolInterventions` — the form is rendered inside the regular tool body (Render slot), not the canonical intervention slot. Cleanest for now: the framework intervention flow assumes `submitToolInteraction` store actions, which would fight our IPC path. We can refactor onto that surface later if CC grows additional interactions (approval, file picker). - Doesn't translate strings — i18n in a follow-up. Type-check clean. Step 6 (real desktop e2e via CC) is next. * ✨ feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up) Step 5 registered the Render component but stopped at the registry — the chat tool-detail still returned the loading placeholder while `isToolCalling` was true, so users only ever saw a spinner during the 5 min intervention window. Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on CC + apiName=askUserQuestion tool messages) and route to the registered builtin Render inline before the placeholder branch. Once the intervention resolves, the eventual `tool_result` clears `pluginState.askUserQuestion` and the regular Render takes over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up) LOBE-8519 left two TODOs in `generationSlice` where hetero runtime silently fell through to client mode — regenerate would secretly hit the agent's underlying LLM, and continue would synthesize a fake "please continue" turn that confuses CC / Codex. - regenerateMessage: re-create the assistant row branched off the same user message, resolve resume sessionId (drop on cwd mismatch), then spawn a child `execHeterogeneousAgent` op so Stop only kills the executor, not the parent regenerate op. Mirrors sendMessage's hetero branch. - continueGenerationMessage: hetero CLIs have no continue primitive — each prompt is a fresh user turn — so bail out instead of polluting the session. - continueGenerationMessage: gateway mode now branches a server-side resume run instead of falling through to client. Surfaced while testing CC AskUserQuestion end-to-end on the LOBE-8725 branch (regenerating after an answered question went through the wrong runtime). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2 Two bugs surfaced when invoking the local-testing helper from a fresh session on macOS: - `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit code propagates through `pipefail`. With `set -e`, an empty pid set silently kills the whole script — `do_start` reported success, no Electron, no error. Trail with `|| true`. - `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`; process-tree teardown still works because `expand_descendants` walks the tree directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725) `AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across every CC subprocess. The SDK transport latches `_initialized=true` after the first `initialize`, so the second op's CC subprocess sees `Invalid Request: Server already initialized` (400) and reports the `lobe_cc` server as `failed`. From the model's POV the MCP tool is absent — it falls back to ToolSearch, can't find anything, and verbalizes the question instead. Refactor to the canonical multi-tenant pattern: one transport + one `McpServer` per session, looked up by the SDK-managed `mcp-session-id` header. New transports are minted on the first POST without a session id (must be an `initialize` request); subsequent requests route via the stored map; `onsessionclosed` cleans up. The first run of any process still works as before — this only matters once a second op spins up. Added a 3-op sequential regression test that fails on the old single-transport implementation and passes now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725) Step 5's first cut shoehorned the pending form into the Render slot and drove submit/skip with a custom `pluginState.askUserQuestion.status` field, which forced three layers of glue: - `Tool/Detail` had to bypass the loading placeholder via an identifier+apiName hardcode so the form would surface during `isToolCalling` - The executor had to `messageService.getMessages → replaceMessages` after `agent_intervention_request` to drag the freshly-created tool row into in-memory state (the framework's own `tool_end → fetchAndReplaceMessages` only fires after the user answers) - The executor also had to `associateMessageWithOperation` for the tool row so the form could look up the running CC op for IPC All three were patches around skipping the canonical surface. This commit moves AskUserQuestion onto `pluginIntervention.status='pending'` and the `BuiltinToolInterventions` registry, which the framework already drives end-to-end: - `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx` — pure form, no IPC, no store reads. Resolves through the standard `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback. - `Render/AskUserQuestion` shrinks to the answered/aborted view only; the framework hides Render while pending, so no status switching. - New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}" chip in the inline tool body, matching the rest of CC's tools. - Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`; `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry. Hetero needs a different action handler than `submitToolInteraction` (which spawns `executeClientAgent` — wrong for a CC subprocess that's already blocked on an MCP call). Two thin pieces wire that: - `submitHeteroIntervention` (chat store) — sets `pluginIntervention` via `optimisticUpdateMessagePlugin` (which already syncs DB + in-memory + parent-assistant `tools[].intervention` in one shot), then forwards the answer through `heterogeneousAgentService.submitIntervention` IPC. Operation lookup walks the tool message's `parentId` to hit the assistant's `messageOperationMap` entry — drops the explicit `associateMessageWithOperation` call from the executor. - `customInteractionHandlers.isHeteroInteractionIdentifier` flags `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits there before reaching the existing `submitToolInteraction` path. Executor change collapses to one line: `optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`. The post-intervention refresh, the associate call, and the `persistInterventionRequest` helper all go away. Removed: - `AskUserQuestionPluginState` type (custom field is gone) - `Tool/Detail` `askUserPending` inline-render branch - Executor `messageService.getMessages + replaceMessages` round-trip - Executor `associateMessageWithOperation` for tool rows - `persistInterventionRequest` helper Verified end-to-end against a real CC subprocess on desktop: - Inline body shows the new Inspector chip; pending form lives in the bottom InterventionBar (canonical surface) - Submit ships answer through MCP, CC continues with structured result - Skip flips status to `rejected`, framework's RejectedResponse shows "User skipped"; CC receives isError and falls back to text - `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op (the per-session transport fix from the previous commit) - `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725) Select dropdown was the wrong primitive — it hides options behind an extra click and doesn't read like a question to answer. CC's underlying tool is 1-4 questions × 2-4 options, so the whole option set always fits inline. - Each option renders as a clickable card: numbered chip (1/2/3/4) + bold label + secondary description on a single row. Hover tints the background; selected state lights up `colorPrimary` on both the chip and the card outline so the pick is unmistakable at a glance. - Multi-select (`q.multiSelect`) toggles instead of replacing, with a "(multi-select)" hint in the question header. - Multi-question support gets a proper visual hierarchy: each question past the first sits below a dashed divider, headed by a `Q1/N` tag + the original `q.header` chip. The `Q*/N` lets the user track progress without counting. - Inspector picks up the question count too: now shows "askUserQuestion · {first header} +N" when multiple are queued. Verified end-to-end on desktop with a CC-driven 2-question prompt (4-option + 3-option). Both selections feed back to CC as a single "User answers" payload, CC echoes both picks in its continuation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725) - Multi-question forms now use a top tab strip; single question renders inline. - Picking a single-select option auto-advances to the next unanswered question. - Drafts persist to tool message `pluginState.askUserDraft` so picks survive remount / HMR; new `setInterventionDraft` action on the chat store dispatches the pluginState patch. - Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for every unanswered question instead of letting the bridge time out into a cancelled isError — model gets a structured answer it can act on. - Visual: selected option now uses filled `colorPrimaryBg` + right-aligned check icon; index chip stays neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725) The async exit-handler cleanup raced Electron's main-process teardown and left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync unlink in the quit hook is the only reliable guarantee. Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q or `app.quit()`, not on external kills (test harness, OS shutdown). Verified by manual test: pending askUserQuestion forms now leave zero residue after both Cmd+Q and SIGTERM paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725) Submit now writes the structured `{ questionText: pickedLabel(s) }` payload to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so Render no longer has to scrape the bridge's prose `User answers:` content. Render shows one Q&A block per question — header + question + a checkmark card per picked option (multi-select fans out into multiple rows). Falls back to a `—` placeholder when answers are missing (older messages or skipped flows), and keeps the existing `pluginError` warning for cancel / no-answer paths. Also surfaces the answers in the Skill state inspector tab, which was previously empty for completed askUserQuestion messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725) Locks down the regression fixed in c0de0cd — async exit-handler cleanup losing to Electron's main-process teardown. Four cases: `before-quit` (Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown), `SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not throw on the second pass). `process.on` and `process.exit` are stubbed in the signal-path tests so the controller's listener attaches to a spy, not the test runner's process — otherwise we'd leak a real SIGTERM listener every test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
emaxlele
pushed a commit
to emaxlele/lobehub
that referenced
this pull request
May 12, 2026
…obehub#14639) * ✨ feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2) Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's built-in tool short-circuits in `-p` mode, so we host an in-process MCP server that exposes an equivalent `ask_user_question` tool. The handler blocks until the consumer submits an answer (or the 5min deadline / op shutdown fires), surfacing a structured `agent_intervention_request` / `agent_intervention_response` round-trip on the existing event stream. Added in this commit: - `packages/heterogeneous-agents/src/askUser/` - `AskUserBridge` — per-op pending map with timeout / cancel / progress keepalive support; emits an async-iterable of outbound events - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server, `?op=<id>` query routes via `AsyncLocalStorage` → `onsessioninitialized` → sessionId↔opId map; tool handler hands off to the matching bridge and pumps `notifications/progress` back to CC every 30s as wire-level keepalive (required for >5min waits, see spike notes) - `constants.ts` — shared tool/server names + the stable `apiName` the adapter rewrites to - Unit tests cover bridge lifecycle (resolve / cancel / timeout / progress / event stream) and an end-to-end MCP probe via `StreamableHTTPClientTransport` - `packages/agent-gateway-client/src/types.ts` — wire-level `agent_intervention_request` / `agent_intervention_response` event variants + payload interfaces. Re-exported through the package barrel. - `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter rewrites `apiName` to `askUserQuestion` so the renderer routes on a clean domain key. Identifier stays `claude-code`. Applied to both the main-agent and subagent paths for symmetry (subagent ask isn't expected today, but doesn't hurt). - `src/server/routers/lambda/aiAgent.ts` — Zod input schema for `aiAgent.heteroIngest` extended with the two new event types so the CLI sandbox can forward them through the server. No producer wiring yet — Steps 3-5 plug this into Electron main, the renderer executor, and the new UI. * ✨ feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3) Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now goes live during real prompts; renderer-submitted answers route back via new IPC. Changes - `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so controller-managed temp configs flow into the driver. - `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts` — append `--mcp-config <path>` when provided. Disallowed-tools pin stays so CC's built-in AskUserQuestion remains off (avoids double- registration of the same tool name). - `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts` - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt (de-duped concurrent first-callers via in-flight promise). - Per-op `setupInterventionForOp(opId, sessionId)`: registers an `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()` into the existing `heteroAgentEvent` broadcast. - Cleanup paths: exit handler `await intervention.cleanup()` settles pending MCP handlers + unlinks the temp config; pre-spawn errors short-circuit the same cleanup so we don't leak bridges on `buildSpawnPlan` / trace-session failures. - `before-quit` stops the MCP server (in addition to killing CC processes). - New `@IpcMethod() submitIntervention({ operationId, toolCallId, result?, cancelled?, cancelReason? })` — renderer side will dispatch answers / cancellations through this in Step 4/5. - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`. - `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy for `submitIntervention`. - New `claudeCode.test.ts` covers the four driver-arg paths (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay disallowed). Existing 28 controller tests still pass. What still doesn't run end-to-end - The renderer `heteroExecutor` doesn't consume `agent_intervention_request` yet — events go through the broadcast but the chat store ignores them. - No UI to render the intervention card or to call `submitIntervention`. Both lands in Steps 4/5 next. * ✨ feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4) Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId` from MCP `_meta`) instead of a random UUID, so the `agent_intervention_request` event references the same id as the existing tool message on the renderer side. Renderer-side `heteroExecutor` learns the new event: - Added `persistInterventionRequest(...)` next to `persistToolResult` — stamps `pluginState.askUserQuestion` (apiName + identifier + questions parsed from `arguments` + deadline + status='pending' + toolCallId) onto the matching tool message via `messageService.updateToolMessage`. - New branch in `handleStreamEvent` for `'agent_intervention_request'`: defers behind `persistQueue` (so it lands AFTER `persistToolBatch` populates `toolMsgIdByCallId`), then mirrors the same pluginState onto the in-memory message via `internal_dispatchMessage` so the UI lights up immediately — no fetchAndReplaceMessages round-trip needed. - The eventual `tool_result` for the same toolCallId hits the existing `tool_result` branch unchanged: it overwrites `pluginState` with whatever the result carries (typically undefined for our MCP tool, so `pluginState.askUserQuestion` clears and the intervention UI yields to the regular Render). Bridge tests cover the new contract: - caller-supplied toolCallId becomes the wire correlation key - duplicate-toolCallId pendings reject loudly so two-handler clobbers surface immediately 153 package tests + 1167 desktop main tests + 51 hetero executor tests still green; type-check clean. * ✨ feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5) Dedicated Render for the synthetic `askUserQuestion` apiName the adapter rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives under CC's render registry so the existing chat tool-detail flow picks it up automatically — no changes to the conversation framework. - New `AskUserQuestionItem` / `AskUserQuestionArgs` / `AskUserQuestionPluginState` types (mirrors CC's own AskUserQuestion schema verbatim). - `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'` member so the renders / inspectors / streamings registries can key off the same enum value. - `client/Render/AskUserQuestion/index.tsx` is the component: - `pluginState.askUserQuestion?.status === 'pending'` → renders the questions form (Select for single-select, CheckboxGroup for multi-select), a 5-min countdown ticking once a second, Submit / Skip buttons. Reads `operationId` via `messageOperationMap` so we can route through `heterogeneousAgentService.submitIntervention`. - Otherwise → renders the questions as muted captions plus the final answer text from `content`. Surfaces a warning when the tool_result was an error (timeout / cancelled / session ended). - Submit button stays disabled until every question has a selection; Skip always enabled (sends `cancelled: true`). - `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers the new component. What this does NOT do - Doesn't touch `BuiltinToolInterventions` — the form is rendered inside the regular tool body (Render slot), not the canonical intervention slot. Cleanest for now: the framework intervention flow assumes `submitToolInteraction` store actions, which would fight our IPC path. We can refactor onto that surface later if CC grows additional interactions (approval, file picker). - Doesn't translate strings — i18n in a follow-up. Type-check clean. Step 6 (real desktop e2e via CC) is next. * ✨ feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up) Step 5 registered the Render component but stopped at the registry — the chat tool-detail still returned the loading placeholder while `isToolCalling` was true, so users only ever saw a spinner during the 5 min intervention window. Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on CC + apiName=askUserQuestion tool messages) and route to the registered builtin Render inline before the placeholder branch. Once the intervention resolves, the eventual `tool_result` clears `pluginState.askUserQuestion` and the regular Render takes over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up) LOBE-8519 left two TODOs in `generationSlice` where hetero runtime silently fell through to client mode — regenerate would secretly hit the agent's underlying LLM, and continue would synthesize a fake "please continue" turn that confuses CC / Codex. - regenerateMessage: re-create the assistant row branched off the same user message, resolve resume sessionId (drop on cwd mismatch), then spawn a child `execHeterogeneousAgent` op so Stop only kills the executor, not the parent regenerate op. Mirrors sendMessage's hetero branch. - continueGenerationMessage: hetero CLIs have no continue primitive — each prompt is a fresh user turn — so bail out instead of polluting the session. - continueGenerationMessage: gateway mode now branches a server-side resume run instead of falling through to client. Surfaced while testing CC AskUserQuestion end-to-end on the LOBE-8725 branch (regenerating after an answered question went through the wrong runtime). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2 Two bugs surfaced when invoking the local-testing helper from a fresh session on macOS: - `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit code propagates through `pipefail`. With `set -e`, an empty pid set silently kills the whole script — `do_start` reported success, no Electron, no error. Trail with `|| true`. - `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`; process-tree teardown still works because `expand_descendants` walks the tree directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725) `AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across every CC subprocess. The SDK transport latches `_initialized=true` after the first `initialize`, so the second op's CC subprocess sees `Invalid Request: Server already initialized` (400) and reports the `lobe_cc` server as `failed`. From the model's POV the MCP tool is absent — it falls back to ToolSearch, can't find anything, and verbalizes the question instead. Refactor to the canonical multi-tenant pattern: one transport + one `McpServer` per session, looked up by the SDK-managed `mcp-session-id` header. New transports are minted on the first POST without a session id (must be an `initialize` request); subsequent requests route via the stored map; `onsessionclosed` cleans up. The first run of any process still works as before — this only matters once a second op spins up. Added a 3-op sequential regression test that fails on the old single-transport implementation and passes now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725) Step 5's first cut shoehorned the pending form into the Render slot and drove submit/skip with a custom `pluginState.askUserQuestion.status` field, which forced three layers of glue: - `Tool/Detail` had to bypass the loading placeholder via an identifier+apiName hardcode so the form would surface during `isToolCalling` - The executor had to `messageService.getMessages → replaceMessages` after `agent_intervention_request` to drag the freshly-created tool row into in-memory state (the framework's own `tool_end → fetchAndReplaceMessages` only fires after the user answers) - The executor also had to `associateMessageWithOperation` for the tool row so the form could look up the running CC op for IPC All three were patches around skipping the canonical surface. This commit moves AskUserQuestion onto `pluginIntervention.status='pending'` and the `BuiltinToolInterventions` registry, which the framework already drives end-to-end: - `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx` — pure form, no IPC, no store reads. Resolves through the standard `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback. - `Render/AskUserQuestion` shrinks to the answered/aborted view only; the framework hides Render while pending, so no status switching. - New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}" chip in the inline tool body, matching the rest of CC's tools. - Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`; `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry. Hetero needs a different action handler than `submitToolInteraction` (which spawns `executeClientAgent` — wrong for a CC subprocess that's already blocked on an MCP call). Two thin pieces wire that: - `submitHeteroIntervention` (chat store) — sets `pluginIntervention` via `optimisticUpdateMessagePlugin` (which already syncs DB + in-memory + parent-assistant `tools[].intervention` in one shot), then forwards the answer through `heterogeneousAgentService.submitIntervention` IPC. Operation lookup walks the tool message's `parentId` to hit the assistant's `messageOperationMap` entry — drops the explicit `associateMessageWithOperation` call from the executor. - `customInteractionHandlers.isHeteroInteractionIdentifier` flags `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits there before reaching the existing `submitToolInteraction` path. Executor change collapses to one line: `optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`. The post-intervention refresh, the associate call, and the `persistInterventionRequest` helper all go away. Removed: - `AskUserQuestionPluginState` type (custom field is gone) - `Tool/Detail` `askUserPending` inline-render branch - Executor `messageService.getMessages + replaceMessages` round-trip - Executor `associateMessageWithOperation` for tool rows - `persistInterventionRequest` helper Verified end-to-end against a real CC subprocess on desktop: - Inline body shows the new Inspector chip; pending form lives in the bottom InterventionBar (canonical surface) - Submit ships answer through MCP, CC continues with structured result - Skip flips status to `rejected`, framework's RejectedResponse shows "User skipped"; CC receives isError and falls back to text - `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op (the per-session transport fix from the previous commit) - `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725) Select dropdown was the wrong primitive — it hides options behind an extra click and doesn't read like a question to answer. CC's underlying tool is 1-4 questions × 2-4 options, so the whole option set always fits inline. - Each option renders as a clickable card: numbered chip (1/2/3/4) + bold label + secondary description on a single row. Hover tints the background; selected state lights up `colorPrimary` on both the chip and the card outline so the pick is unmistakable at a glance. - Multi-select (`q.multiSelect`) toggles instead of replacing, with a "(multi-select)" hint in the question header. - Multi-question support gets a proper visual hierarchy: each question past the first sits below a dashed divider, headed by a `Q1/N` tag + the original `q.header` chip. The `Q*/N` lets the user track progress without counting. - Inspector picks up the question count too: now shows "askUserQuestion · {first header} +N" when multiple are queued. Verified end-to-end on desktop with a CC-driven 2-question prompt (4-option + 3-option). Both selections feed back to CC as a single "User answers" payload, CC echoes both picks in its continuation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725) - Multi-question forms now use a top tab strip; single question renders inline. - Picking a single-select option auto-advances to the next unanswered question. - Drafts persist to tool message `pluginState.askUserDraft` so picks survive remount / HMR; new `setInterventionDraft` action on the chat store dispatches the pluginState patch. - Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for every unanswered question instead of letting the bridge time out into a cancelled isError — model gets a structured answer it can act on. - Visual: selected option now uses filled `colorPrimaryBg` + right-aligned check icon; index chip stays neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725) The async exit-handler cleanup raced Electron's main-process teardown and left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync unlink in the quit hook is the only reliable guarantee. Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q or `app.quit()`, not on external kills (test harness, OS shutdown). Verified by manual test: pending askUserQuestion forms now leave zero residue after both Cmd+Q and SIGTERM paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725) Submit now writes the structured `{ questionText: pickedLabel(s) }` payload to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so Render no longer has to scrape the bridge's prose `User answers:` content. Render shows one Q&A block per question — header + question + a checkmark card per picked option (multi-select fans out into multiple rows). Falls back to a `—` placeholder when answers are missing (older messages or skipped flows), and keeps the existing `pluginError` warning for cancel / no-answer paths. Also surfaces the answers in the Skill state inspector tab, which was previously empty for completed askUserQuestion messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725) Locks down the regression fixed in c0de0cd — async exit-handler cleanup losing to Electron's main-process teardown. Four cases: `before-quit` (Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown), `SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not throw on the second pass). `process.on` and `process.exit` are stubbed in the signal-path tests so the controller's listener attaches to a spy, not the test runner's process — otherwise we'd leak a real SIGTERM listener every test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
arvinxx
added a commit
that referenced
this pull request
May 12, 2026
…14639) * ✨ feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2) Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's built-in tool short-circuits in `-p` mode, so we host an in-process MCP server that exposes an equivalent `ask_user_question` tool. The handler blocks until the consumer submits an answer (or the 5min deadline / op shutdown fires), surfacing a structured `agent_intervention_request` / `agent_intervention_response` round-trip on the existing event stream. Added in this commit: - `packages/heterogeneous-agents/src/askUser/` - `AskUserBridge` — per-op pending map with timeout / cancel / progress keepalive support; emits an async-iterable of outbound events - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server, `?op=<id>` query routes via `AsyncLocalStorage` → `onsessioninitialized` → sessionId↔opId map; tool handler hands off to the matching bridge and pumps `notifications/progress` back to CC every 30s as wire-level keepalive (required for >5min waits, see spike notes) - `constants.ts` — shared tool/server names + the stable `apiName` the adapter rewrites to - Unit tests cover bridge lifecycle (resolve / cancel / timeout / progress / event stream) and an end-to-end MCP probe via `StreamableHTTPClientTransport` - `packages/agent-gateway-client/src/types.ts` — wire-level `agent_intervention_request` / `agent_intervention_response` event variants + payload interfaces. Re-exported through the package barrel. - `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter rewrites `apiName` to `askUserQuestion` so the renderer routes on a clean domain key. Identifier stays `claude-code`. Applied to both the main-agent and subagent paths for symmetry (subagent ask isn't expected today, but doesn't hurt). - `src/server/routers/lambda/aiAgent.ts` — Zod input schema for `aiAgent.heteroIngest` extended with the two new event types so the CLI sandbox can forward them through the server. No producer wiring yet — Steps 3-5 plug this into Electron main, the renderer executor, and the new UI. * ✨ feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3) Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now goes live during real prompts; renderer-submitted answers route back via new IPC. Changes - `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so controller-managed temp configs flow into the driver. - `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts` — append `--mcp-config <path>` when provided. Disallowed-tools pin stays so CC's built-in AskUserQuestion remains off (avoids double- registration of the same tool name). - `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts` - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt (de-duped concurrent first-callers via in-flight promise). - Per-op `setupInterventionForOp(opId, sessionId)`: registers an `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()` into the existing `heteroAgentEvent` broadcast. - Cleanup paths: exit handler `await intervention.cleanup()` settles pending MCP handlers + unlinks the temp config; pre-spawn errors short-circuit the same cleanup so we don't leak bridges on `buildSpawnPlan` / trace-session failures. - `before-quit` stops the MCP server (in addition to killing CC processes). - New `@IpcMethod() submitIntervention({ operationId, toolCallId, result?, cancelled?, cancelReason? })` — renderer side will dispatch answers / cancellations through this in Step 4/5. - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`. - `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy for `submitIntervention`. - New `claudeCode.test.ts` covers the four driver-arg paths (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay disallowed). Existing 28 controller tests still pass. What still doesn't run end-to-end - The renderer `heteroExecutor` doesn't consume `agent_intervention_request` yet — events go through the broadcast but the chat store ignores them. - No UI to render the intervention card or to call `submitIntervention`. Both lands in Steps 4/5 next. * ✨ feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4) Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId` from MCP `_meta`) instead of a random UUID, so the `agent_intervention_request` event references the same id as the existing tool message on the renderer side. Renderer-side `heteroExecutor` learns the new event: - Added `persistInterventionRequest(...)` next to `persistToolResult` — stamps `pluginState.askUserQuestion` (apiName + identifier + questions parsed from `arguments` + deadline + status='pending' + toolCallId) onto the matching tool message via `messageService.updateToolMessage`. - New branch in `handleStreamEvent` for `'agent_intervention_request'`: defers behind `persistQueue` (so it lands AFTER `persistToolBatch` populates `toolMsgIdByCallId`), then mirrors the same pluginState onto the in-memory message via `internal_dispatchMessage` so the UI lights up immediately — no fetchAndReplaceMessages round-trip needed. - The eventual `tool_result` for the same toolCallId hits the existing `tool_result` branch unchanged: it overwrites `pluginState` with whatever the result carries (typically undefined for our MCP tool, so `pluginState.askUserQuestion` clears and the intervention UI yields to the regular Render). Bridge tests cover the new contract: - caller-supplied toolCallId becomes the wire correlation key - duplicate-toolCallId pendings reject loudly so two-handler clobbers surface immediately 153 package tests + 1167 desktop main tests + 51 hetero executor tests still green; type-check clean. * ✨ feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5) Dedicated Render for the synthetic `askUserQuestion` apiName the adapter rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives under CC's render registry so the existing chat tool-detail flow picks it up automatically — no changes to the conversation framework. - New `AskUserQuestionItem` / `AskUserQuestionArgs` / `AskUserQuestionPluginState` types (mirrors CC's own AskUserQuestion schema verbatim). - `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'` member so the renders / inspectors / streamings registries can key off the same enum value. - `client/Render/AskUserQuestion/index.tsx` is the component: - `pluginState.askUserQuestion?.status === 'pending'` → renders the questions form (Select for single-select, CheckboxGroup for multi-select), a 5-min countdown ticking once a second, Submit / Skip buttons. Reads `operationId` via `messageOperationMap` so we can route through `heterogeneousAgentService.submitIntervention`. - Otherwise → renders the questions as muted captions plus the final answer text from `content`. Surfaces a warning when the tool_result was an error (timeout / cancelled / session ended). - Submit button stays disabled until every question has a selection; Skip always enabled (sends `cancelled: true`). - `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers the new component. What this does NOT do - Doesn't touch `BuiltinToolInterventions` — the form is rendered inside the regular tool body (Render slot), not the canonical intervention slot. Cleanest for now: the framework intervention flow assumes `submitToolInteraction` store actions, which would fight our IPC path. We can refactor onto that surface later if CC grows additional interactions (approval, file picker). - Doesn't translate strings — i18n in a follow-up. Type-check clean. Step 6 (real desktop e2e via CC) is next. * ✨ feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up) Step 5 registered the Render component but stopped at the registry — the chat tool-detail still returned the loading placeholder while `isToolCalling` was true, so users only ever saw a spinner during the 5 min intervention window. Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on CC + apiName=askUserQuestion tool messages) and route to the registered builtin Render inline before the placeholder branch. Once the intervention resolves, the eventual `tool_result` clears `pluginState.askUserQuestion` and the regular Render takes over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up) LOBE-8519 left two TODOs in `generationSlice` where hetero runtime silently fell through to client mode — regenerate would secretly hit the agent's underlying LLM, and continue would synthesize a fake "please continue" turn that confuses CC / Codex. - regenerateMessage: re-create the assistant row branched off the same user message, resolve resume sessionId (drop on cwd mismatch), then spawn a child `execHeterogeneousAgent` op so Stop only kills the executor, not the parent regenerate op. Mirrors sendMessage's hetero branch. - continueGenerationMessage: hetero CLIs have no continue primitive — each prompt is a fresh user turn — so bail out instead of polluting the session. - continueGenerationMessage: gateway mode now branches a server-side resume run instead of falling through to client. Surfaced while testing CC AskUserQuestion end-to-end on the LOBE-8725 branch (regenerating after an answered question went through the wrong runtime). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2 Two bugs surfaced when invoking the local-testing helper from a fresh session on macOS: - `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit code propagates through `pipefail`. With `set -e`, an empty pid set silently kills the whole script — `do_start` reported success, no Electron, no error. Trail with `|| true`. - `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`; process-tree teardown still works because `expand_descendants` walks the tree directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725) `AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across every CC subprocess. The SDK transport latches `_initialized=true` after the first `initialize`, so the second op's CC subprocess sees `Invalid Request: Server already initialized` (400) and reports the `lobe_cc` server as `failed`. From the model's POV the MCP tool is absent — it falls back to ToolSearch, can't find anything, and verbalizes the question instead. Refactor to the canonical multi-tenant pattern: one transport + one `McpServer` per session, looked up by the SDK-managed `mcp-session-id` header. New transports are minted on the first POST without a session id (must be an `initialize` request); subsequent requests route via the stored map; `onsessionclosed` cleans up. The first run of any process still works as before — this only matters once a second op spins up. Added a 3-op sequential regression test that fails on the old single-transport implementation and passes now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725) Step 5's first cut shoehorned the pending form into the Render slot and drove submit/skip with a custom `pluginState.askUserQuestion.status` field, which forced three layers of glue: - `Tool/Detail` had to bypass the loading placeholder via an identifier+apiName hardcode so the form would surface during `isToolCalling` - The executor had to `messageService.getMessages → replaceMessages` after `agent_intervention_request` to drag the freshly-created tool row into in-memory state (the framework's own `tool_end → fetchAndReplaceMessages` only fires after the user answers) - The executor also had to `associateMessageWithOperation` for the tool row so the form could look up the running CC op for IPC All three were patches around skipping the canonical surface. This commit moves AskUserQuestion onto `pluginIntervention.status='pending'` and the `BuiltinToolInterventions` registry, which the framework already drives end-to-end: - `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx` — pure form, no IPC, no store reads. Resolves through the standard `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback. - `Render/AskUserQuestion` shrinks to the answered/aborted view only; the framework hides Render while pending, so no status switching. - New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}" chip in the inline tool body, matching the rest of CC's tools. - Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`; `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry. Hetero needs a different action handler than `submitToolInteraction` (which spawns `executeClientAgent` — wrong for a CC subprocess that's already blocked on an MCP call). Two thin pieces wire that: - `submitHeteroIntervention` (chat store) — sets `pluginIntervention` via `optimisticUpdateMessagePlugin` (which already syncs DB + in-memory + parent-assistant `tools[].intervention` in one shot), then forwards the answer through `heterogeneousAgentService.submitIntervention` IPC. Operation lookup walks the tool message's `parentId` to hit the assistant's `messageOperationMap` entry — drops the explicit `associateMessageWithOperation` call from the executor. - `customInteractionHandlers.isHeteroInteractionIdentifier` flags `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits there before reaching the existing `submitToolInteraction` path. Executor change collapses to one line: `optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`. The post-intervention refresh, the associate call, and the `persistInterventionRequest` helper all go away. Removed: - `AskUserQuestionPluginState` type (custom field is gone) - `Tool/Detail` `askUserPending` inline-render branch - Executor `messageService.getMessages + replaceMessages` round-trip - Executor `associateMessageWithOperation` for tool rows - `persistInterventionRequest` helper Verified end-to-end against a real CC subprocess on desktop: - Inline body shows the new Inspector chip; pending form lives in the bottom InterventionBar (canonical surface) - Submit ships answer through MCP, CC continues with structured result - Skip flips status to `rejected`, framework's RejectedResponse shows "User skipped"; CC receives isError and falls back to text - `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op (the per-session transport fix from the previous commit) - `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725) Select dropdown was the wrong primitive — it hides options behind an extra click and doesn't read like a question to answer. CC's underlying tool is 1-4 questions × 2-4 options, so the whole option set always fits inline. - Each option renders as a clickable card: numbered chip (1/2/3/4) + bold label + secondary description on a single row. Hover tints the background; selected state lights up `colorPrimary` on both the chip and the card outline so the pick is unmistakable at a glance. - Multi-select (`q.multiSelect`) toggles instead of replacing, with a "(multi-select)" hint in the question header. - Multi-question support gets a proper visual hierarchy: each question past the first sits below a dashed divider, headed by a `Q1/N` tag + the original `q.header` chip. The `Q*/N` lets the user track progress without counting. - Inspector picks up the question count too: now shows "askUserQuestion · {first header} +N" when multiple are queued. Verified end-to-end on desktop with a CC-driven 2-question prompt (4-option + 3-option). Both selections feed back to CC as a single "User answers" payload, CC echoes both picks in its continuation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725) - Multi-question forms now use a top tab strip; single question renders inline. - Picking a single-select option auto-advances to the next unanswered question. - Drafts persist to tool message `pluginState.askUserDraft` so picks survive remount / HMR; new `setInterventionDraft` action on the chat store dispatches the pluginState patch. - Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for every unanswered question instead of letting the bridge time out into a cancelled isError — model gets a structured answer it can act on. - Visual: selected option now uses filled `colorPrimaryBg` + right-aligned check icon; index chip stays neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725) The async exit-handler cleanup raced Electron's main-process teardown and left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync unlink in the quit hook is the only reliable guarantee. Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q or `app.quit()`, not on external kills (test harness, OS shutdown). Verified by manual test: pending askUserQuestion forms now leave zero residue after both Cmd+Q and SIGTERM paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725) Submit now writes the structured `{ questionText: pickedLabel(s) }` payload to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so Render no longer has to scrape the bridge's prose `User answers:` content. Render shows one Q&A block per question — header + question + a checkmark card per picked option (multi-select fans out into multiple rows). Falls back to a `—` placeholder when answers are missing (older messages or skipped flows), and keeps the existing `pluginError` warning for cancel / no-answer paths. Also surfaces the answers in the Skill state inspector tab, which was previously empty for completed askUserQuestion messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725) Locks down the regression fixed in c0de0cd — async exit-handler cleanup losing to Electron's main-process teardown. Four cases: `before-quit` (Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown), `SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not throw on the second pass). `process.on` and `process.exit` are stubbed in the signal-path tests so the controller's listener attaches to a spy, not the test runner's process — otherwise we'd leak a real SIGTERM listener every test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 12, 2026
Closed
Merged
arvinxx
added a commit
that referenced
this pull request
May 13, 2026
# 🚀 LobeHub Release (20260513) **Hotfix Scope:** Ship the canary backlog (111 PRs) onto main as a fast-tracked patch — operator-focused, no weekly-style write-up. > Brings the accumulated canary work into main: agent/task improvements, hetero-agent fixes, desktop & onboarding polish, and several reliability caps. ## ✨ What's Included - **Agent & tasks** — Self-review proposal-to-action automation, sub-agent dispatch consolidated to `lobe-agent`, AskUserQuestion wiring for Claude Code, scheduler/hotkey/TodoList polish. (#14583, #14657, #14715, #14639, #14732, #14707, #14713) - **Home & onboarding** — Daily brief with linkable welcome + paired input hint, inline skill auth in recommended task templates, cleanup of captcha-on-signin and marketplace early-exit. (#14589, #14676, #14573, #14598) - **Bots & integrations** — Slack MPIM support, Discord DM fix, slash-command + connect-error fixes, gateway client-tool plugin state. (#14733, #14591, #14596) - **Desktop & CLI** — Windows `.cmd` shim detection for `claude` / `codex` CLIs, auth focus & pending-login reset fixes. (#14720, #14694, #14695) - **Reliability** — Cap web-crawler body size and image binary at safe limits, attach error listeners to Neon/Node pools, reject inactive OIDC access. (#14660, #14711, #14606, #14674) - **Database** — `agent_operations` table + persist agent operations from the runtime; switch user memory search to `paradedb.match(...)`. (#14416, #14736, #14590) ## ⚙️ Upgrade - **Self-hosted:** pull the latest image and restart. Drizzle migrations (including the new `agent_operations` table) run automatically on boot.
lezi-fun
pushed a commit
to lezi-fun/lobehub
that referenced
this pull request
May 13, 2026
…obehub#14639) * ✨ feat(hetero-agent): AskUserQuestion MCP server + bridge skeleton (LOBE-8725 step 1+2) Foundation for LOBE-8725 — interactive AskUserQuestion via local MCP. CC's built-in tool short-circuits in `-p` mode, so we host an in-process MCP server that exposes an equivalent `ask_user_question` tool. The handler blocks until the consumer submits an answer (or the 5min deadline / op shutdown fires), surfacing a structured `agent_intervention_request` / `agent_intervention_response` round-trip on the existing event stream. Added in this commit: - `packages/heterogeneous-agents/src/askUser/` - `AskUserBridge` — per-op pending map with timeout / cancel / progress keepalive support; emits an async-iterable of outbound events - `AskUserMcpServer` — process-wide HTTP/Streamable MCP server, `?op=<id>` query routes via `AsyncLocalStorage` → `onsessioninitialized` → sessionId↔opId map; tool handler hands off to the matching bridge and pumps `notifications/progress` back to CC every 30s as wire-level keepalive (required for >5min waits, see spike notes) - `constants.ts` — shared tool/server names + the stable `apiName` the adapter rewrites to - Unit tests cover bridge lifecycle (resolve / cancel / timeout / progress / event stream) and an end-to-end MCP probe via `StreamableHTTPClientTransport` - `packages/agent-gateway-client/src/types.ts` — wire-level `agent_intervention_request` / `agent_intervention_response` event variants + payload interfaces. Re-exported through the package barrel. - `packages/heterogeneous-agents/src/adapters/claudeCode.ts` — when CC's `tool_use` carries `mcp__lobe_cc__ask_user_question`, the adapter rewrites `apiName` to `askUserQuestion` so the renderer routes on a clean domain key. Identifier stays `claude-code`. Applied to both the main-agent and subagent paths for symmetry (subagent ask isn't expected today, but doesn't hurt). - `src/server/routers/lambda/aiAgent.ts` — Zod input schema for `aiAgent.heteroIngest` extended with the two new event types so the CLI sandbox can forward them through the server. No producer wiring yet — Steps 3-5 plug this into Electron main, the renderer executor, and the new UI. * ✨ feat(hetero-agent): wire AskUserQuestion MCP into Electron CC driver (LOBE-8725 step 3) Plug the Step 1 skeleton (`AskUserMcpServer` + `AskUserBridge`) into the desktop Claude Code spawn path. CC's local MCP `ask_user_question` tool now goes live during real prompts; renderer-submitted answers route back via new IPC. Changes - `apps/desktop/src/main/modules/heterogeneousAgent/types.ts` — add optional `mcpConfigPath` to `HeterogeneousAgentBuildPlanParams` so controller-managed temp configs flow into the driver. - `apps/desktop/src/main/modules/heterogeneousAgent/drivers/claudeCode.ts` — append `--mcp-config <path>` when provided. Disallowed-tools pin stays so CC's built-in AskUserQuestion remains off (avoids double- registration of the same tool name). - `apps/desktop/src/main/controllers/HeterogeneousAgentCtr.ts` - Lazy-singleton `AskUserMcpServer` started on first claude-code prompt (de-duped concurrent first-callers via in-flight promise). - Per-op `setupInterventionForOp(opId, sessionId)`: registers an `AskUserBridge`, writes `os.tmpdir()/lobe-cc-mcp-<opId>.json` with `alwaysLoad: true` so CC eager-loads the tool (1-hop call, no ToolSearch detour — see LOBE-8725 spike), pumps `bridge.events()` into the existing `heteroAgentEvent` broadcast. - Cleanup paths: exit handler `await intervention.cleanup()` settles pending MCP handlers + unlinks the temp config; pre-spawn errors short-circuit the same cleanup so we don't leak bridges on `buildSpawnPlan` / trace-session failures. - `before-quit` stops the MCP server (in addition to killing CC processes). - New `@IpcMethod() submitIntervention({ operationId, toolCallId, result?, cancelled?, cancelReason? })` — renderer side will dispatch answers / cancellations through this in Step 4/5. - codex unchanged — bridge setup is gated on `agentType === 'claude-code'`. - `src/services/electron/heterogeneousAgent.ts` — renderer-side proxy for `submitIntervention`. - New `claudeCode.test.ts` covers the four driver-arg paths (`--mcp-config` presence, ordering vs `--resume`, AskUserQuestion stay disallowed). Existing 28 controller tests still pass. What still doesn't run end-to-end - The renderer `heteroExecutor` doesn't consume `agent_intervention_request` yet — events go through the broadcast but the chat store ignores them. - No UI to render the intervention card or to call `submitIntervention`. Both lands in Steps 4/5 next. * ✨ feat(hetero-agent): correlate intervention with tool message + renderer handler (LOBE-8725 step 3.5+4) Bridge now uses the caller-supplied toolCallId (CC's `claudecode/toolUseId` from MCP `_meta`) instead of a random UUID, so the `agent_intervention_request` event references the same id as the existing tool message on the renderer side. Renderer-side `heteroExecutor` learns the new event: - Added `persistInterventionRequest(...)` next to `persistToolResult` — stamps `pluginState.askUserQuestion` (apiName + identifier + questions parsed from `arguments` + deadline + status='pending' + toolCallId) onto the matching tool message via `messageService.updateToolMessage`. - New branch in `handleStreamEvent` for `'agent_intervention_request'`: defers behind `persistQueue` (so it lands AFTER `persistToolBatch` populates `toolMsgIdByCallId`), then mirrors the same pluginState onto the in-memory message via `internal_dispatchMessage` so the UI lights up immediately — no fetchAndReplaceMessages round-trip needed. - The eventual `tool_result` for the same toolCallId hits the existing `tool_result` branch unchanged: it overwrites `pluginState` with whatever the result carries (typically undefined for our MCP tool, so `pluginState.askUserQuestion` clears and the intervention UI yields to the regular Render). Bridge tests cover the new contract: - caller-supplied toolCallId becomes the wire correlation key - duplicate-toolCallId pendings reject loudly so two-handler clobbers surface immediately 153 package tests + 1167 desktop main tests + 51 hetero executor tests still green; type-check clean. * ✨ feat(claude-code): AskUserQuestion intervention render component (LOBE-8725 step 5) Dedicated Render for the synthetic `askUserQuestion` apiName the adapter rewrites the local MCP `mcp__lobe_cc__ask_user_question` tool to. Lives under CC's render registry so the existing chat tool-detail flow picks it up automatically — no changes to the conversation framework. - New `AskUserQuestionItem` / `AskUserQuestionArgs` / `AskUserQuestionPluginState` types (mirrors CC's own AskUserQuestion schema verbatim). - `ClaudeCodeApiName` gains an `AskUserQuestion = 'askUserQuestion'` member so the renders / inspectors / streamings registries can key off the same enum value. - `client/Render/AskUserQuestion/index.tsx` is the component: - `pluginState.askUserQuestion?.status === 'pending'` → renders the questions form (Select for single-select, CheckboxGroup for multi-select), a 5-min countdown ticking once a second, Submit / Skip buttons. Reads `operationId` via `messageOperationMap` so we can route through `heterogeneousAgentService.submitIntervention`. - Otherwise → renders the questions as muted captions plus the final answer text from `content`. Surfaces a warning when the tool_result was an error (timeout / cancelled / session ended). - Submit button stays disabled until every question has a selection; Skip always enabled (sends `cancelled: true`). - `ClaudeCodeRenders[ClaudeCodeApiName.AskUserQuestion]` registers the new component. What this does NOT do - Doesn't touch `BuiltinToolInterventions` — the form is rendered inside the regular tool body (Render slot), not the canonical intervention slot. Cleanest for now: the framework intervention flow assumes `submitToolInteraction` store actions, which would fight our IPC path. We can refactor onto that surface later if CC grows additional interactions (approval, file picker). - Doesn't translate strings — i18n in a follow-up. Type-check clean. Step 6 (real desktop e2e via CC) is next. * ✨ feat(claude-code): render AskUserQuestion form during pending state (LOBE-8725 step 5 follow-up) Step 5 registered the Render component but stopped at the registry — the chat tool-detail still returned the loading placeholder while `isToolCalling` was true, so users only ever saw a spinner during the 5 min intervention window. Detect `pluginState.askUserQuestion?.status === 'pending'` (only set on CC + apiName=askUserQuestion tool messages) and route to the registered builtin Render inline before the placeholder branch. Once the intervention resolves, the eventual `tool_result` clears `pluginState.askUserQuestion` and the regular Render takes over. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(hetero-agent): wire regenerate / continue for hetero runtime (LOBE-8519 follow-up) LOBE-8519 left two TODOs in `generationSlice` where hetero runtime silently fell through to client mode — regenerate would secretly hit the agent's underlying LLM, and continue would synthesize a fake "please continue" turn that confuses CC / Codex. - regenerateMessage: re-create the assistant row branched off the same user message, resolve resume sessionId (drop on cwd mismatch), then spawn a child `execHeterogeneousAgent` op so Stop only kills the executor, not the parent regenerate op. Mirrors sendMessage's hetero branch. - continueGenerationMessage: hetero CLIs have no continue primitive — each prompt is a fresh user turn — so bail out instead of polluting the session. - continueGenerationMessage: gateway mode now branches a server-side resume run instead of falling through to client. Surfaced while testing CC AskUserQuestion end-to-end on the LOBE-8725 branch (regenerating after an answered question went through the wrong runtime). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(local-testing): electron-dev.sh boots on macOS bash 3.2 Two bugs surfaced when invoking the local-testing helper from a fresh session on macOS: - `find_project_pids` / `do_stop` end with `grep -v '^$'` whose exit code propagates through `pipefail`. With `set -e`, an empty pid set silently kills the whole script — `do_start` reported success, no Electron, no error. Trail with `|| true`. - `setsid` is GNU coreutils, not on macOS. Fall back to plain `bash -c`; process-tree teardown still works because `expand_descendants` walks the tree directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): per-session MCP transport for sequential ops (LOBE-8725) `AskUserMcpServer` shared a single `StreamableHTTPServerTransport` across every CC subprocess. The SDK transport latches `_initialized=true` after the first `initialize`, so the second op's CC subprocess sees `Invalid Request: Server already initialized` (400) and reports the `lobe_cc` server as `failed`. From the model's POV the MCP tool is absent — it falls back to ToolSearch, can't find anything, and verbalizes the question instead. Refactor to the canonical multi-tenant pattern: one transport + one `McpServer` per session, looked up by the SDK-managed `mcp-session-id` header. New transports are minted on the first POST without a session id (must be an `initialize` request); subsequent requests route via the stored map; `onsessionclosed` cleans up. The first run of any process still works as before — this only matters once a second op spins up. Added a 3-op sequential regression test that fails on the old single-transport implementation and passes now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ♻️ refactor(claude-code): move AskUserQuestion onto canonical Intervention surface (LOBE-8725) Step 5's first cut shoehorned the pending form into the Render slot and drove submit/skip with a custom `pluginState.askUserQuestion.status` field, which forced three layers of glue: - `Tool/Detail` had to bypass the loading placeholder via an identifier+apiName hardcode so the form would surface during `isToolCalling` - The executor had to `messageService.getMessages → replaceMessages` after `agent_intervention_request` to drag the freshly-created tool row into in-memory state (the framework's own `tool_end → fetchAndReplaceMessages` only fires after the user answers) - The executor also had to `associateMessageWithOperation` for the tool row so the form could look up the running CC op for IPC All three were patches around skipping the canonical surface. This commit moves AskUserQuestion onto `pluginIntervention.status='pending'` and the `BuiltinToolInterventions` registry, which the framework already drives end-to-end: - `packages/builtin-tool-claude-code/src/client/Intervention/AskUserQuestion.tsx` — pure form, no IPC, no store reads. Resolves through the standard `onInteractionAction({type:'submit'|'skip'|'cancel'})` callback. - `Render/AskUserQuestion` shrinks to the answered/aborted view only; the framework hides Render while pending, so no status switching. - New `Inspector/AskUserQuestion` shows a compact "askUserQuestion · {header}" chip in the inline tool body, matching the rest of CC's tools. - Registries: `ClaudeCodeInspectors`, `ClaudeCodeRenders`, and the new `ClaudeCodeInterventions` all key off `ClaudeCodeApiName.AskUserQuestion`; `BuiltinToolInterventions` gains a `[ClaudeCodeIdentifier]` entry. Hetero needs a different action handler than `submitToolInteraction` (which spawns `executeClientAgent` — wrong for a CC subprocess that's already blocked on an MCP call). Two thin pieces wire that: - `submitHeteroIntervention` (chat store) — sets `pluginIntervention` via `optimisticUpdateMessagePlugin` (which already syncs DB + in-memory + parent-assistant `tools[].intervention` in one shot), then forwards the answer through `heterogeneousAgentService.submitIntervention` IPC. Operation lookup walks the tool message's `parentId` to hit the assistant's `messageOperationMap` entry — drops the explicit `associateMessageWithOperation` call from the executor. - `customInteractionHandlers.isHeteroInteractionIdentifier` flags `ClaudeCodeIdentifier`; `Tool/Detail/Intervention` short-circuits there before reaching the existing `submitToolInteraction` path. Executor change collapses to one line: `optimisticUpdateMessagePlugin(toolMsgId, { intervention: { status: 'pending' } })`. The post-intervention refresh, the associate call, and the `persistInterventionRequest` helper all go away. Removed: - `AskUserQuestionPluginState` type (custom field is gone) - `Tool/Detail` `askUserPending` inline-render branch - Executor `messageService.getMessages + replaceMessages` round-trip - Executor `associateMessageWithOperation` for tool rows - `persistInterventionRequest` helper Verified end-to-end against a real CC subprocess on desktop: - Inline body shows the new Inspector chip; pending form lives in the bottom InterventionBar (canonical surface) - Submit ships answer through MCP, CC continues with structured result - Skip flips status to `rejected`, framework's RejectedResponse shows "User skipped"; CC receives isError and falls back to text - `mcp_servers.lobe_cc.status === 'connected'` on a 3rd sequential op (the per-session transport fix from the previous commit) - `alwaysLoad: true` still produces 1-hop calls (no ToolSearch hop) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 💄 style(claude-code): inline numbered option cards for AskUserQuestion intervention (LOBE-8725) Select dropdown was the wrong primitive — it hides options behind an extra click and doesn't read like a question to answer. CC's underlying tool is 1-4 questions × 2-4 options, so the whole option set always fits inline. - Each option renders as a clickable card: numbered chip (1/2/3/4) + bold label + secondary description on a single row. Hover tints the background; selected state lights up `colorPrimary` on both the chip and the card outline so the pick is unmistakable at a glance. - Multi-select (`q.multiSelect`) toggles instead of replacing, with a "(multi-select)" hint in the question header. - Multi-question support gets a proper visual hierarchy: each question past the first sits below a dashed divider, headed by a `Q1/N` tag + the original `q.header` chip. The `Q*/N` lets the user track progress without counting. - Inspector picks up the question count too: now shows "askUserQuestion · {first header} +N" when multiple are queued. Verified end-to-end on desktop with a CC-driven 2-question prompt (4-option + 3-option). Both selections feed back to CC as a single "User answers" payload, CC echoes both picks in its continuation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): tabbed multi-question + draft + timeout fallback for AskUserQuestion (LOBE-8725) - Multi-question forms now use a top tab strip; single question renders inline. - Picking a single-select option auto-advances to the next unanswered question. - Drafts persist to tool message `pluginState.askUserDraft` so picks survive remount / HMR; new `setInterventionDraft` action on the chat store dispatches the pluginState patch. - Timeout fallback: when the 5-min countdown expires, auto-submit option 1 for every unanswered question instead of letting the bridge time out into a cancelled isError — model gets a structured answer it can act on. - Visual: selected option now uses filled `colorPrimaryBg` + right-aligned check icon; index chip stays neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * 🐛 fix(hetero-agent): synchronously unlink temp mcp.json on app quit (LOBE-8725) The async exit-handler cleanup raced Electron's main-process teardown and left `lobe-cc-mcp-<opId>.json` files in `os.tmpdir()` after every quit. Sync unlink in the quit hook is the only reliable guarantee. Also handle SIGTERM / SIGINT — `before-quit` only fires on user-driven Cmd+Q or `app.quit()`, not on external kills (test harness, OS shutdown). Verified by manual test: pending askUserQuestion forms now leave zero residue after both Cmd+Q and SIGTERM paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✨ feat(claude-code): persist structured AskUserQuestion answers + Q&A render (LOBE-8725) Submit now writes the structured `{ questionText: pickedLabel(s) }` payload to the tool message's `pluginState.askUserAnswers` (in-memory + DB merge), so Render no longer has to scrape the bridge's prose `User answers:` content. Render shows one Q&A block per question — header + question + a checkmark card per picked option (multi-select fans out into multiple rows). Falls back to a `—` placeholder when answers are missing (older messages or skipped flows), and keeps the existing `pluginError` warning for cancel / no-answer paths. Also surfaces the answers in the Skill state inspector tab, which was previously empty for completed askUserQuestion messages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ✅ test(hetero-agent): cover synchronous quit cleanup of AskUserQuestion temp configs (LOBE-8725) Locks down the regression fixed in c0de0cd — async exit-handler cleanup losing to Electron's main-process teardown. Four cases: `before-quit` (Cmd+Q / `app.quit()` path), `SIGTERM` (test harness / OS shutdown), `SIGINT` (Ctrl-C), and idempotency (already-deleted temp file must not throw on the second pass). `process.on` and `process.exit` are stubbed in the signal-path tests so the controller's listener attaches to a spy, not the test runner's process — otherwise we'd leak a real SIGTERM listener every test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Merged
arvinxx
added a commit
that referenced
this pull request
May 18, 2026
# 🚀 LobeHub Release (20260518) **Release Date:** May 18, 2026 **Since v2.1.58:** 208 merged PRs · 209 commits · 16 contributors > v2.2.0 introduces the **Chief Agent Operator** — an agent that runs itself end-to-end. It self-iterates against its own output, assembles sub-agent teams on demand through the heterogeneous runtime, and drives a unified task system that knows when to pause for a human. Self-review, AssistantGroup, and tasks/scheduling all converge into one operator surface. --- ## ✨ Highlights ### 🎩 Chief Agent Operator - **Self-iteration exits Lab** — Agent Signal's self-review pipeline ships proposal actions straight into briefs and auto-executes the approved follow-ups, with prompts hardened against eval. The operator now critiques and re-runs its own work without a human in the loop. (#14769, #14583, #14647, #14882) - **Auto-formed agent teams** — Heterogeneous AssistantGroup gains Monitor-style signal callbacks, read-only SubAgent threads with breadcrumb headers, and a thread switcher. The operator dispatches sub-agents and you can step into any branch to see what the team is doing. (#14859, #14658, #14845, #14715) - **Task system as the operator's runway** — Claude Code surfaces task tools, AskUserQuestion freeform notes, and a dedicated `waitingForHuman` topic status; `lobe-task` exposes `setTaskSchedule`; the scheduler is hardened (maxExecutions cap, sub-10min heartbeat block, race-free SchedulerForm). Long-running operator runs no longer go silent and stop themselves when human input is needed. (#14870, #14639, #14713, #14865, #14853) ### 🚀 Cloud & runtime - **Cloud Claude Code V3** — Repo picker, GitHub token flow, and sandbox-aware context bring cloud-hosted Claude Code to feature parity with local; cloud sandbox completion now triggers the task lifecycle end-to-end. (#14568, #14822, #14681) - **Heterogeneous agent multi-replica safety** — Subagent threads, ingest refresh, and parallel-tool counts now survive replica swaps without losing parent_id or rolling back tool state. (#14897, #14631, #14806, #14838) - **Built-in tool lifecycle hooks** — `onBeforeCall` / `onAfterCall` land on the built-in tool runtime; sub-agent dispatch moves to `lobe-agent`; self-iteration aligns with the shared inspector pattern. (#14719, #14715, #14827) - **Knowledge base RAG unified** — Client and server share one `KnowledgeBaseSearchService`; KB files preserved on `NoSuchKey` instead of silently lost. (#14673, #14501) ### 💬 Workspace experience - **Home daily brief + recommendations** — The home screen opens with a linkable welcome, paired input hint, and a recommendations module sourced from the operator's hetero action library. (#14589, #14645, #14770) - **Chat mode + redesigned action bar** — The chat input gains a Chat/Agent mode toggle and a re-pitched action bar with icon-and-color action tag chips. (#14774, #14903, #14846) - **Documents tree, optimistic** — Document tree creates, deletes, and inline renames now apply optimistically; the agent-documents index hides web crawls and switches to a table layout. (#14714, #14292) - **Branded MCP inspectors** — Linear MCP tool calls render with the same branded inspector as the built-in Linear skill; CC MCP and built-in skills now share inspector code. (#14864, #14884) - **Bot identity gating** — Device tools are gated by sender identity, the activator bypass is closed, and Slack mpim plus Discord DM regressions are fixed. (#14634, #14664, #14733) --- ## 🏗️ Core Agent & Signal Pipeline ### Self-iteration & Agent Signal - Self-iteration graduates out of Lab, with service, tool, name, and concept structure unified across `agent-signal`, `prompts`, `database`, and `builtin-tool-self-iteration`. (#14699, #14769) - Self-review now proposes actions to briefs and auto-executes the approved set, with eval-verified prompt hardening. (#14583, #14657, #14647) - Self-iteration built-in tool aligns with the shared runtime + inspector patterns. (#14827) - Agent Signal prompts adapt their response language and avoid blocking agent execution. (#14890, #14775, #14882) - Receipt descriptions now carry an Agent Signal marker, and self-review hinted skill documents route correctly. (#14764, #14895) ### Heterogeneous agent runtime - Subagent threads render read-only with a breadcrumb header and thread switcher; SUBAGENT badge dropped, indentation tightened. (#14658, #14845, #14783) - Multi-replica safety: ingest refresh restores tools/model from DB to fix parent_id breaks; new-step assistants sync across replicas; subagent-tagged events no longer leak into the main gateway handler. (#14897, #14631, #14838) - Fetch-triggering events are deferred to keep parallel tool counts from rolling back. (#14806) - AskUserQuestion is wired for Claude Code, with auto-decline disabled and a freeform note input on the cloud side; `waitingForHuman` is a first-class topic status. (#14639, #14629, #14870) - AssistantGroup gains Monitor-style signal callbacks; project skills surface in the working sidebar and markdown preview. (#14859, #14896) - Cloud Claude Code V3 — repo picker, GitHub token, sandbox context; credentials alert and disabled input when not configured. (#14568, #14822) - Cloud sandbox completion now triggers the task lifecycle end-to-end. (#14681) ### Agent runtime & context engine - Built-in tool runtime gets `onBeforeCall` / `onAfterCall` lifecycle hooks. (#14719) - `CompletionLifecycle`, `HumanInterventionHandler`, and `stepPresentation` are extracted from the runtime monolith. (#14441) - Per-tool timeout is honored end-to-end for client tool dispatch. (#14817) - Compression budget accounts for `tool_calls`, reasoning content, and tool defs; `call_llm` forwards tools into the budget. (#14813, #14837) - Pre-flight context check now fails fast for OpenAI-compatible providers. (#14824) - Malformed `tool_call` names are recovered instead of finishing the step silently. (#14577) - Sub-agent dispatch moves from `lobe-gtd` to `lobe-agent`. (#14715) - Hidden built-in tools now appear in the system prompt @-mention list. (#14823) ### Agent tracing & operations - New `agent_operations` table and runtime persistence for every hetero-agent operation. (#14416, #14736) - `signOperationJwt` issues 4-hour signed operation tokens. (#14586) - S3 trace snapshots are zstd-compressed; DB `trace_s3_key` aligns with the `.json.zst` suffix; legacy `.json` fallback preserved on fetch. (#14807, #14860, #14826) --- ## 📱 Platform & Integrations ### Bot / Channels - Device tools are gated by sender identity. (#14634) - Activator bypass closed and device-access checks converged. (#14664) - Slack mpim supported; Discord DM regression fixed; Slack connect + slash commands repaired. (#14733, #14591) - Bot channels, bot watch, bot callback service, and system bot reliability fixes. (#14847, #14796, #14570, #14784, #14649) - Online Messager scaffolding. (#14755) ### Onboarding - Home daily brief with linkable welcome and paired input hint. (#14589) - Recommendations module sourced from the hetero agent action library. (#14645) - Chat onboarding passes request triggers via metadata and preserves the resume request. (#14770, #14798) - Discovery turn progress gated by phase, with a reminder on stalled discovery. (#14842, #14833) - FullNameStep back button rejoins the shared prefix; ModeSwitch hidden in production. (#14898, #14760) - Agent marketplace folds into the web onboarding tool. (#14578, #14672) - Onboarding interests stored as keys instead of free text; early-exit skips marketplace and drops CJK prompts. (#14624, #14598) ### Model providers - Gemini 3.1 Flash-Lite cards; Gemini schema sanitizer drops non-compliant `enum` / `required`; zero `cachedContentTokenCount` handled in usage conversion. (#14604, #14740, #14567) - DeepSeek-V4 model cards and pricing restored to official rates. (#14110, #14911) - ernie-5.1 and spark-x2-flash support; Grok 4.3 `reasoning_effort` support. (#14643, #14731, #14642) - SiliconCloud catalog synced with API; duplicates removed; reasoning params adjusted. (#14464) - Minimax derives `max_tokens` from context window to avoid `ExceededContextWindow`. (#14814) - aihubmix uses the full models endpoint for a complete list; stale empty-apiKey test dropped. (#14511, #14669) - Stream parse errors are enriched with provider + model context. (#14636) - Visual content parts are consumed in the server runtime; video image references move to a JSON object. (#14637, #14900) - Google function call magic `thoughtSignature` now attaches to every part, not just the last turn. (#14904) - Service model assignments settings added; model extend-param options removed. (#14712, #14607) ### Built-in tools & knowledge base - `lobe-task` exposes `setTaskSchedule`; task scheduler hardened (maxExecutions cap, sub-10min heartbeat blocked, SchedulerForm race fix, rapid automation-mode toggle stabilized). (#14713, #14865, #14853, #14801) - KnowledgeBaseSearchService shares RAG runtime across client and server. (#14673) - KB files preserved on `NoSuchKey` and orphan documents/tasks cleaned. (#14501) - Document tree gets optimistic create/delete + inline rename. (#14714) - agent-documents index hides web crawls and switches to a table layout. (#14292) - `lobe-clarify` and SKILL.md frontmatter parsing/edit validation are unified. (#14566) - AnalyzeVisualMedia inspector + Portal HTML preview refactor; HTML preview restored for AssistantGroup messages. (#14777, #14811) - Branded inspector shared between CC MCP and built-in Linear skill. (#14884, #14864) --- ## 🖥️ CLI & User Experience ### Chat & Conversation - Chat mode toggle and redesigned chat input action bar. (#14774) - Action tag chips switch to icon + colored label; ActionDropdown closes on sibling-open and focus-out; submenu uses native header/footer slots. (#14903, #14802, #14901) - Action bar padding equalized around the send button; skeleton shows in action bar while config loads. (#14846, #14656) - `useCmdEnterToSend` is respected in thread & task inputs; send button enables after pasting into thread/comment input. (#14850, #14816) - TopicChatDrawer state preserved during close animation. (#14803) - Only the last assistant block animates during markdown streaming. (#14906) - Right working panel no longer auto-collapses on chat mount; home agent config fetched so knowledge toggles reflect in UI. (#14883, #14834) ### Tasks - Task scheduler, hotkey, comment, and TodoList polish. (#14707) - Add Subtask button & card baseline aligned; activity card stop run; task agent manager polish. (#14848, #14559, #14569) - Task template skeleton CLS reduced; task page placeholder copy refreshed. (#14788, #14704) - Task agent model snapshotted into `task.config` at create time. (#14670) - User-feedback card, task card polish, and Run-now context menu in markdown. (#14727) - Inline skill auth in recommended task templates. (#14676) ### Navigation & Layout - Tab bar gains a Chrome-style divider between inactive tabs. (#14892) - SideBarDrawer & header layout polish; nav ActionIcon sizing unified; TodoList encapsulation improved. (#14762, #14692) - Desktop header icons, sidebar density, and task menus polished. (#14724) - Standardized header action icon sizes. (#14717) - Chat topic title length increased; copy session ID added to topic dropdown menu. (#14659, #14595) - Heterogeneous agent topic rows regain indentation. (#14783) ### Other polish - Usage token details shortened; tool execution time formatted as `Xmin Ys`. (#14849, #14641) - Tool arguments display gets word-wrap toggle; long tool-call params wrap instead of truncate. (#14706, #14640) - Editor stops showing per-line placeholder once content is present. (#14852) - Visible divider between queued messages; intervention confirmation bar polished. (#14593, #14587) - Credit top-up copy refreshed; auth captcha retry copy refreshed; brief recommendations layout polished. (#14821, #14561, #14871) --- ## 🔧 Tooling & Developer Experience - Dev-only feature flag override panel. (#14565) - `__DEV__` define replaces `process.env.NODE_ENV` in the SPA. (#14696) - Agent-settings drops Meta/Documents tabs and restores `inputTemplate`. (#14874) - `local-system` forwards all `grepContent` params and moves the executor to `/client`. (#14888) - `lobe-task` and `setTaskSchedule` exposed. (#14713) - Memory user-memory benchmark agent config and source-id extraction schemas. (#14779, #14778) - CLI man page drops stale cron entry; `clearMessages` hotkey removed. (#14709, #14906) - Skill docs simplified; cloud heteroContext gains sandbox TTL + public-repo fork push guide. (#14785, #14761) --- ## 🔒 Security & Reliability - **Security:** Sensitive comments and examples sanitized from the production JS bundle. (#14557) - **Security:** Inactive OIDC access rejected. (#14674) - **Security:** CASC `new Function()` template replaced with safe string builders. (#14751) - **Security:** Sign-in captcha flow removed in favor of safer flow. (#14573) - **Security:** Desktop local file previews restricted to safe roots. (#14789) - **Security:** Image binary capped at 3.75 MB so base64 payload stays under the Anthropic 5 MB limit. (#14711) - **Reliability:** Neon/Node pools get error listeners to prevent Lambda crashes. (#14606) - **Reliability:** `paradedb.match(...)` replaces hardcoded normalizer in memory search. (#14590) - **Reliability:** `PlaceholderVariablesProcessor` errors carry diagnostic context. (#14741) - **Reliability:** File storage upload checks are serialized; multiple account link bug fixed. (#14829, #14562) - **Reliability:** `ScrollShadow` replaced with `ScrollArea` to fix a React infinite render loop (error code 185). (#14689) - **Reliability:** Embedding token cap enforced — long memory queries are limited and truncated before search. (#14757) - **Reliability:** Embed binary blob guard + oversized output cap in `local-system.readFile`. (#14602) - **Reliability:** Windows npm CLI shims resolved before spawning agents. (#14772, #14720) - **Reliability:** Vite pinned to 8.0.12 to avoid the rolldown 1.0.1 preload regression; desktop runtime externals split from native deps. (#14804, #14776) - **Reliability:** Old lobehub cron job removed; WeChat URL rules dropped from web crawler. (#14630, #14633) --- ## 👥 Contributors Huge thanks to **16 contributors** who shipped **208 merged PRs** this cycle. @hezhijie0327 · @sxjeru · @hardy-one · @Bianzinan · @brone1323 · @YuSaZh · @Wxh16144 · @arvinxx · @Innei · @tjx666 · @neko · @lijian · @rdmclin2 · @sudongyuer · @AmAzing129 · @rivertwilight Plus @lobehubbot for maintenance translations. --- **Full Changelog**: v2.1.58...v2.2.0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
💻 Change Type
🔗 Related Issue
Part of LOBE-8725 — full plan + spike findings in the issue.
🔀 Description of Change
Foundation layer for interactive AskUserQuestion in the CC integration. CC's built-in
AskUserQuestionshort-circuits in-pmode (PR #14629 already disables it as a stop-gap); this PR introduces the local MCP server + per-op bridge that an upcoming Step 3 will wire into Electron'sHeterogeneousAgentCtr.What lands here
packages/heterogeneous-agents/src/askUser/AskUserBridge— per-op pending map.pending(args, opts)returns a Promise the MCP handler awaits; producer side callsresolve(toolCallId, ...)/cancel(...)/cancelAll(...). Built-in 5-min timeout, 30snotifications/progresskeepalive cadence, async-iterableevents()for the producer to forward.AskUserMcpServer— process-wide HTTP/Streamable MCP host. One server, many ops: each spawn callsregisterOperation(opId)and gets a URL with?op=<id>. Routing happens viaAsyncLocalStorage→onsessioninitialized→sessionId↔opIdmap; tool handler readsextra.sessionIdto find the matching bridge. Stateful session id (sessionIdGenerator: randomUUID) — empirically required, CC rejects stateless mode. Tool registration mirrors CC's built-inAskUserQuestionschema verbatim (1-4 questions × 2-4 options, optional multiSelect).constants.ts— shared MCP server / tool / apiName strings consumed by both server and adapter.StreamableHTTPClientTransport.packages/agent-gateway-client/src/types.ts— wire-levelagent_intervention_request/agent_intervention_responseevent variants +AgentInterventionRequestData/AgentInterventionResponseDatapayload interfaces. Re-exported through the package barrel.packages/heterogeneous-agents/src/adapters/claudeCode.ts— when CC'stool_usecarriesmcp__lobe_cc__ask_user_question, the adapter rewritesapiNametoaskUserQuestion. Identifier staysclaude-code. Applied on both the main-agent and subagent paths for symmetry.src/server/routers/lambda/aiAgent.ts— Zod schema foraiAgent.heteroIngestextended with the two new event types so the CLI sandbox can forward them.Spike background (linked from LOBE-8725)
notifications/progressevery 30s as wire-level keepalive — without it CC drops the SSE around 5 min withtransport dropped mid-call.alwaysLoad: trueon the--mcp-configserver entry promotes the MCP tools to eager-loaded (skip ToolSearch). Soft dependency — falls back to deferred + ToolSearch if the field stops working.What does NOT land here
No producer wiring yet. Steps 3-6 will:
AskUserMcpServerinto ElectronHeterogeneousAgentCtr(start/stop, per-opregisterOperation, write/cleanup tempmcp.json, append--mcp-configto driver args)submitInterventionIPC and renderer-sideagent_intervention_requesthandlinglobe-user-interaction, kept under CC's domain)lh hetero execkeeps current text-fallback behavior — phase 2🧪 How to Test
The new bridge + server tests exercise:
pending()resolve / cancel / 5-min timeout / progress notification cadence?op=URL paramStreamableHTTPClientTransportNo runtime path is invoked yet — the producer (Electron / sandbox) doesn't reference any of the new code in this PR. Behavior change lands in Step 3.
📝 Additional Information
Followups tracked in LOBE-8725 acceptance checklist. Sandbox CLI inbound back-channel design is deferred to phase 2 (separate issue).