Summary
Calling the team_task_list MCP tool causes the calling agent's session to hang / become unresponsive (and spawned sub-agents to crash). This is already reported as a symptom in iOfficeAI/AionUi#3136, but without a root cause. After auditing the aionui-team crate, I believe I've found the concrete differentiating cause and a one-line-style fix that mirrors an existing guard the team already applied to team_members.
Environment
- AionCore: built from
bundled-aioncore shipped with AionUi (observed on the v0.1.17 / v0.1.18 line; code paths below are present on main)
- OS: Windows 11 (x64)
- Affected backends: most visible with Gemini-backed teammates; the teammate system prompt (
crates/aionui-team/src/prompts/teammate.rs) explicitly instructs new teammates to "Use team_task_list and team_members to check current team state.", so freshly spawned teammates hit this almost immediately.
Steps to Reproduce
- Create a team with at least one teammate (any spawned sub-agent).
- Have the teammate call
team_task_list (it will do so on its own, per the teammate system prompt).
- The teammate's turn never completes — the session hangs / goes unresponsive. The Lead cannot detect the dead sub-agent.
Expected
team_task_list returns the task list and the agent turn completes normally, exactly like team_members does.
Actual
The calling agent hangs. team_members and team_send_message (called the same way) do not hang.
Root Cause Analysis
This is NOT a server-side deadlock. I traced the call path and the team_task_list path actually takes fewer locks than team_members:
crates/aionui-team/src/mcp/server.rs → dispatch_tool → exec_task_list(scheduler).await
- →
scheduler::list_tasks → task_board.list_tasks(&team_id).await → repo.list_tasks(...).await
TaskBoard holds only repo: Arc<dyn ITeamRepository> — no Mutex/RwLock, no lock held across an .await.
The actual differentiator is null fields in the tool result. In exec_task_list, the response object is built with the json! macro directly:
json!({
"id": t.id,
"subject": t.subject,
"description": t.description, // Option<String> -> serializes to JSON `null` when None
"status": t.status,
"owner": t.owner, // Option<String> -> serializes to JSON `null` when unassigned
"blocked_by": t.blocked_by,
"blocks": t.blocks,
})
Even though TeamTask carries #[serde(skip_serializing_if = "Option::is_none")] on its fields, that attribute is bypassed here because the response is hand-built with json! instead of serializing the struct. So description: null / owner: null are emitted to the MCP client.
The smoking gun: exec_members in the same file already guards against exactly this, with a comment explaining why:
// ...so MCP clients never see `null` and misread a live teammate as offline.
let status = a.status.unwrap_or(TeammateStatus::Idle);
So the project has already hit "null in an MCP tool result breaks the client" once, fixed it for team_members, but team_task_list was never given the same treatment. team_members / team_send_message don't hang precisely because their results contain no null (send_message returns a plain string).
The client-side failure mode (Gemini/ACP turn handling choking on null/undefined tool-call results and leaving an orphaned, never-completing tool call) is consistent with several existing front-end guards in iOfficeAI/AionUi (e.g. PRs that ignore tool calls without call IDs / non-renderable stream events) and with known Gemini null/undefined functionResponse issues.
Evidence strength (being honest)
- "Not a server-side deadlock;
list_tasks takes fewer locks than list_agents" — confirmed from source
- "
exec_task_list emits null; exec_members deliberately avoids null" — confirmed from source
- "The
null is what hangs the front-end agent turn" — strong inference (backed by the existing exec_members guard + comment, the front-end guard PRs, and known Gemini null-handling issues), but I have not captured a runtime trace pinning the exact front-end crash line.
Suggested Fix (mirrors the existing exec_members guard)
In crates/aionui-team/src/mcp/server.rs, exec_task_list, stop emitting bare null for the optional string fields:
"description": t.description.clone().unwrap_or_default(),
"owner": t.owner.clone().unwrap_or_default(),
(or build the json! object conditionally so None keys are omitted). This makes team_task_list output null-free, consistent with team_members. A more thorough/defensive fix would also harden the front-end tool-result normalization so that any MCP tool returning null can't strand a tool call — but the server-side change above is the minimal, low-risk fix matching a pattern the team has already validated.
Related
- Symptom report:
iOfficeAI/AionUi#3136 ("Bug: team_task_list tool causes spawned sub-agents to crash")
Summary
Calling the
team_task_listMCP tool causes the calling agent's session to hang / become unresponsive (and spawned sub-agents to crash). This is already reported as a symptom iniOfficeAI/AionUi#3136, but without a root cause. After auditing theaionui-teamcrate, I believe I've found the concrete differentiating cause and a one-line-style fix that mirrors an existing guard the team already applied toteam_members.Environment
bundled-aioncoreshipped with AionUi (observed on thev0.1.17/v0.1.18line; code paths below are present onmain)crates/aionui-team/src/prompts/teammate.rs) explicitly instructs new teammates to "Useteam_task_listandteam_membersto check current team state.", so freshly spawned teammates hit this almost immediately.Steps to Reproduce
team_task_list(it will do so on its own, per the teammate system prompt).Expected
team_task_listreturns the task list and the agent turn completes normally, exactly liketeam_membersdoes.Actual
The calling agent hangs.
team_membersandteam_send_message(called the same way) do not hang.Root Cause Analysis
This is NOT a server-side deadlock. I traced the call path and the
team_task_listpath actually takes fewer locks thanteam_members:crates/aionui-team/src/mcp/server.rs→dispatch_tool→exec_task_list(scheduler).awaitscheduler::list_tasks→task_board.list_tasks(&team_id).await→repo.list_tasks(...).awaitTaskBoardholds onlyrepo: Arc<dyn ITeamRepository>— no Mutex/RwLock, no lock held across an.await.The actual differentiator is
nullfields in the tool result. Inexec_task_list, the response object is built with thejson!macro directly:Even though
TeamTaskcarries#[serde(skip_serializing_if = "Option::is_none")]on its fields, that attribute is bypassed here because the response is hand-built withjson!instead of serializing the struct. Sodescription: null/owner: nullare emitted to the MCP client.The smoking gun:
exec_membersin the same file already guards against exactly this, with a comment explaining why:So the project has already hit "null in an MCP tool result breaks the client" once, fixed it for
team_members, butteam_task_listwas never given the same treatment.team_members/team_send_messagedon't hang precisely because their results contain nonull(send_message returns a plain string).The client-side failure mode (Gemini/ACP turn handling choking on
null/undefinedtool-call results and leaving an orphaned, never-completing tool call) is consistent with several existing front-end guards iniOfficeAI/AionUi(e.g. PRs that ignore tool calls without call IDs / non-renderable stream events) and with known Gemininull/undefinedfunctionResponse issues.Evidence strength (being honest)
list_taskstakes fewer locks thanlist_agents" — confirmed from sourceexec_task_listemitsnull;exec_membersdeliberately avoidsnull" — confirmed from sourcenullis what hangs the front-end agent turn" — strong inference (backed by the existingexec_membersguard + comment, the front-end guard PRs, and known Gemini null-handling issues), but I have not captured a runtime trace pinning the exact front-end crash line.Suggested Fix (mirrors the existing
exec_membersguard)In
crates/aionui-team/src/mcp/server.rs,exec_task_list, stop emitting barenullfor the optional string fields:(or build the
json!object conditionally soNonekeys are omitted). This makesteam_task_listoutput null-free, consistent withteam_members. A more thorough/defensive fix would also harden the front-end tool-result normalization so that any MCP tool returningnullcan't strand a tool call — but the server-side change above is the minimal, low-risk fix matching a pattern the team has already validated.Related
iOfficeAI/AionUi#3136("Bug: team_task_list tool causes spawned sub-agents to crash")