fix: resolve subagent type showing _unknown while running#57
fix: resolve subagent type showing _unknown while running#57JayantDevkar merged 6 commits intomainfrom
Conversation
Add prompt-based matching (Phase 2.5) to get_all_subagent_types() as a fallback for running agents where no tool_result exists yet. Compares each unmatched agent's initial prompt against Task tool_use input prompts from the parent session JSONL. Also filter internal classification markers (_unknown, _warmup) from the API response in _determine_subagent_type() so the frontend shows "Other" instead of raw internal strings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JayantDevkar
left a comment
There was a problem hiding this comment.
Walkthrough of the changes for review. The core idea: we already have the agent's prompt and the Task input prompt available before the agent finishes — we just weren't matching them.
| # Phase 2.5: Match unmatched agents by prompt content | ||
| # Fallback for running agents where no tool_result exists yet. | ||
| # Compares each unmatched agent's initial prompt against Task input prompts. | ||
| unmatched_agent_files = [f for f in agent_files if f.stem.removeprefix("agent-") not in result] |
There was a problem hiding this comment.
Why this exists: Before this change, an agent only got its type after Phase 1 matched its agentId from a tool_result. But tool_result is only written when the agent finishes. This new phase runs before the prefix/content classification (Phase 3-4), so running agents get typed correctly instead of falling through to _unknown.
We only build the prompt lookup table when there are actually unmatched agents — no extra JSONL scanning for sessions where everything already resolved.
| agent_id = agent_file.stem.removeprefix("agent-") | ||
| agent_prompt = _get_agent_initial_prompt(agent_file) | ||
| if agent_prompt: | ||
| key = _normalize_key(agent_prompt) |
There was a problem hiding this comment.
How the matching works: The Task input.prompt field contains the exact text that becomes the agent's first UserMessage. We normalize both sides (lowercase + collapse whitespace) and compare the first 100 chars. This is the same approach collect_subagent_info() in collectors.py:469-471 already uses for the session subagents tab — we're just bringing it to the shared extraction function.
Edge case: if two Task calls have identical first-100-char prompts but different types, the last one wins. In practice this doesn't happen since prompts are unique per spawn.
| return " ".join(text.lower().strip().split()) | ||
|
|
||
|
|
||
| def _extract_task_prompt_types(path: Path) -> dict[str, str]: |
There was a problem hiding this comment.
Three new helpers — kept minimal and self-contained:
_normalize_key()— same logic asutils.normalize_key(), duplicated here to keepsubagent_types.pydependency-free (it only imports json/logging/re/Path)_extract_task_prompt_types()— single-pass JSONL scan, same pattern as_extract_types_from_raw_jsonl()above_get_agent_initial_prompt()— reads only the first line of the agent JSONL, same pattern as_classify_by_first_message()
| return types.get(agent_id) | ||
| result = types.get(agent_id) | ||
| # Convert internal classification markers to None for the API response | ||
| if result and result.startswith("_"): |
There was a problem hiding this comment.
Defensive filter: get_all_subagent_types() uses internal markers like _unknown, _warmup, _teammate, _compact for classification. These should never leak into the API response. Previously _unknown was returned verbatim and the frontend displayed it as a badge label. Now any _-prefixed result becomes null, which the frontend renders as "Other" via getSubagentTypeDisplayName().
…dle, smart polling (#58, #59, #60, #61) Gap 1 (#58): Add server-side live session fetch to agent detail page so status badge renders immediately on page load instead of waiting for client-side polling. Gap 4 (#61): Add GET /agents/{encoded}/{uuid}/agents/{agentId}/live-status endpoint that extracts subagent status from parent session's state file. Gap 3 (#60): Derive active/idle from entity end_time for running agents using 15s idle threshold. Reuses session statusConfig colors. Gap 5 (#59): Stop refreshing agent JSONL data after subagent completes while continuing live-status polling at 5s rate for badge accuracy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously, the live-sessions fetch was awaited sequentially before the Promise.all, adding an unnecessary round-trip to every agent detail page load. All 6 fetches are independent, so they should fire simultaneously. Also fixes ruff formatting in subagent_sessions.py and adds review.md with remaining polish suggestions for the PR.
Remove review.md — PR review artifact, not project documentation. Fix /live-status endpoint docstring to accurately document the three response cases (404, 200 with subagent=null, 200 with subagent data). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes #56, #58, #59, #60, #61
What's broken
When you open a subagent's detail page while it's still running, the type badge shows
_unknowninstead of the actual type (e.g.oh-my-claudecode:critic). Once the agent finishes, the type magically appears. Beyond that, the agent status system has several gaps compared to session status.Issues addressed
_unknownwhile runningRoot cause (#56)
We figure out which agent belongs to which Task tool call by matching the
agentIdfield inside thetool_resultmessage. Problem is, thattool_resultonly gets written to the parent session JSONL after the agent finishes. So while it's running, we have no way to link agent → Task → type, and it falls through to the default_unknownclassification.Changes
1. Prompt-based type matching (#56)
Added Phase 2.5 to
get_all_subagent_types()— for any agent that's still unmatched after tool_result extraction:Also added a guard in
_determine_subagent_type()to convert internal markers (_unknown,_warmup) tonullin the API response.2. SSR live session pre-fetch (#58)
Added
liveSessionfetch toagents/[agent_id]/+page.server.tsso the status badge appears immediately on page load instead of waiting for client-side polling.3. Smart polling termination (#59)
Added logic in
ConversationView.svelteto stop refreshing agent data once the subagent has completed, even if the parent session is still running.4. Active/idle status for agents (#60)
Frontend heuristic: derive
activevsidlefrom the agent's last message timestamp. No hook changes needed.5. Dedicated agent live-status endpoint (#61)
Added
/agents/{encoded}/{uuid}/agents/{agentId}/live-statusthat reads the parent session's state file and returns just the subagent's status entry.Testing
ruff check)Files changed
api/services/subagent_types.pyapi/routers/subagent_sessions.py_markers from API response + live-status endpointfrontend/src/routes/projects/[project_slug]/[session_slug]/agents/[agent_id]/+page.server.tsfrontend/src/routes/projects/[project_slug]/[session_slug]/agents/[agent_id]/+page.sveltefrontend/src/lib/components/conversation/ConversationView.sveltefrontend/src/lib/components/conversation/ConversationHeader.svelte