Skip to content

fix: resolve subagent type showing _unknown while running#57

Merged
JayantDevkar merged 6 commits intomainfrom
fix/56-subagent-type-unknown-while-running
Apr 14, 2026
Merged

fix: resolve subagent type showing _unknown while running#57
JayantDevkar merged 6 commits intomainfrom
fix/56-subagent-type-unknown-while-running

Conversation

@JayantDevkar
Copy link
Copy Markdown
Owner

@JayantDevkar JayantDevkar commented Apr 13, 2026

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 _unknown instead 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

Issue Gap Description
#56 Type detection Agent type shows _unknown while running
#58 SSR pre-fetch Agent page missing server-side live session data
#59 Smart polling Agent polling doesn't stop after agent completes
#60 Status granularity No active/idle distinction for running agents
#61 API endpoint No dedicated agent live-status endpoint

Root cause (#56)

We figure out which agent belongs to which Task tool call by matching the agentId field inside the tool_result message. Problem is, that tool_result only 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 _unknown classification.

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:

  1. Read its first JSONL line to grab the initial prompt (first 100 chars)
  2. Normalize it (lowercase, collapse whitespace)
  3. Compare against Task tool_use input prompts from the parent session

Also added a guard in _determine_subagent_type() to convert internal markers (_unknown, _warmup) to null in the API response.

2. SSR live session pre-fetch (#58)

Added liveSession fetch to agents/[agent_id]/+page.server.ts so the status badge appears immediately on page load instead of waiting for client-side polling.

3. Smart polling termination (#59)

Added logic in ConversationView.svelte to 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 active vs idle from the agent's last message timestamp. No hook changes needed.

5. Dedicated agent live-status endpoint (#61)

Added /agents/{encoded}/{uuid}/agents/{agentId}/live-status that reads the parent session's state file and returns just the subagent's status entry.

Testing

  • All 1474 existing tests pass
  • Verified prompt matching against real session data
  • Lint clean (ruff check)

Files changed

File What changed
api/services/subagent_types.py Phase 2.5 prompt matching + 3 helper functions
api/routers/subagent_sessions.py Filter _ markers from API response + live-status endpoint
frontend/src/routes/projects/[project_slug]/[session_slug]/agents/[agent_id]/+page.server.ts SSR live session fetch
frontend/src/routes/projects/[project_slug]/[session_slug]/agents/[agent_id]/+page.svelte Pass liveSession prop
frontend/src/lib/components/conversation/ConversationView.svelte Smart polling termination
frontend/src/lib/components/conversation/ConversationHeader.svelte Active/idle heuristic for agents

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>
Copy link
Copy Markdown
Owner Author

@JayantDevkar JayantDevkar left a comment

Choose a reason for hiding this comment

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

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]
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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)
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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]:
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Three new helpers — kept minimal and self-contained:

  • _normalize_key() — same logic as utils.normalize_key(), duplicated here to keep subagent_types.py dependency-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("_"):
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

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().

JayantDevkar and others added 3 commits April 13, 2026 19:48
…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>
the-non-expert and others added 2 commits April 14, 2026 10:17
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>
@JayantDevkar JayantDevkar merged commit 4067d87 into main Apr 14, 2026
20 checks passed
@JayantDevkar JayantDevkar deleted the fix/56-subagent-type-unknown-while-running branch April 14, 2026 15:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: subagent type shows as _unknown while agent is running

2 participants