Skip to content

fix(scanner): name background subagent dispatches instead of 'unknown' - #176

Open
retog wants to merge 1 commit into
phuryn:DEVfrom
retog:feat/name-async-subagents
Open

fix(scanner): name background subagent dispatches instead of 'unknown'#176
retog wants to merge 1 commit into
phuryn:DEVfrom
retog:feat/name-async-subagents

Conversation

@retog

@retog retog commented Aug 30, 2026

Copy link
Copy Markdown

The bug

Most of my subagent dispatches show as unknown in Top Subagent Dispatches and in the by-type breakdown, even though the transcripts clearly identify them.

extract_agent_dispatch learns an agent's name from the parent's closing toolUseResult, which carries agentType only when the dispatch ran synchronously and completed. A background (isAsync) dispatch logs a launch record instead:

{"isAsync": true, "status": "async_launched", "agentId": "a777b13086edbcf85",
 "description": "...", "resolvedModel": "claude-sonnet-5", "prompt": "..."}

No agentType. So those agents never get a row in agents, the LEFT JOIN misses, and AGENT_TYPE_EXPR falls through to 'unknown'.

Across my transcript tree (2,123 files), of 120 parent records carrying an agentId:

status count has agentType
completed 40 yes
async_launched 80 no

Two thirds of dispatches were unnameable from the parent side alone.

The fix

Every assistant record inside the subagent's own jsonl carries attributionAgent alongside agentId — the same human-readable name (general-purpose, Explore, plugin agents like health:coach). It is present regardless of how the dispatch was launched or whether it ever finished, so it recovers the name in exactly the cases the parent record can't.

  • New extract_agent_attribution() returns a name-only record, tagged source: "attribution".
  • upsert_agents() now routes by source: full dispatch records overwrite every column as before; attribution records use COALESCE(NULLIF(agents.agent_type, ''), excluded.agent_type) so they never clobber a name learned from a parent agentType. Dispatches are applied first, so the outcome doesn't depend on file scan order.
  • Wired into both parse paths (parse_jsonl_file and the incremental tail parser in scan()).

Backfill

Files already in processed_files are skipped by an incremental scan, so existing databases would keep showing unknown indefinitely. _backfill_agent_types() mirrors the existing _backfill_topics() exactly — gated on a schema_meta marker (agent_type_backfill_done), re-reads only attributionAgent records with a cheap substring prefilter, and leaves turns untouched so token totals cannot drift.

Verification

On a real 2,123-file transcript tree, before → after:

TYPE               dispatches        tokens
general-purpose            33    69,587,479
archivist                  38    68,021,644
health:coach               28    34,735,377
health:medic               18    29,085,953
Explore                    10     9,695,270
herald                      4     2,940,891
claude                      1     1,408,369
ari:ari                     2       753,841

All 134 dispatches named, zero unknown — where only 40 were reachable from a parent agentType.

The backfill path was exercised on a simulated pre-fix database: 40 → 134 named agents with 0 new/updated files (2,122 skipped), confirming it works without a rescan.

Tests

6 new tests in tests/test_subagent.py (TestAgentAttribution), covering extraction, the both-fields-required guard, parse capture, dispatch-name-wins precedence, the async end-to-end case, and the backfill on an already-scanned DB. Full suite: 153 passed.

Versioning

Added a ## v1.5.6 — TBD heading with the CHANGELOG bullets and bumped scanner.VERSION + vscode-extension/package.json in lockstep, since tests/test_version.py enforces parity. Happy to change the bump level or drop the heading entirely if you'd rather write it yourself at release time.


Written with an agent, reviewed by Reto.

The scanner learned a subagent's name only from the parent's closing
toolUseResult, which carries agentType only when the dispatch ran
synchronously and completed. A background (isAsync) dispatch logs
status: 'async_launched' with no agentType at all, so those agents got no
row in the agents table and rendered as 'unknown' in Top Subagent
Dispatches and the by-type breakdown.

Every assistant record inside the subagent's own jsonl carries
attributionAgent alongside agentId, so the name is recoverable regardless
of how the dispatch was launched or whether it ever finished. Read it
there too, as a name-only record; the parent's dispatch record stays the
source for stats and status and its agentType is never clobbered.

Includes a one-time backfill gated on schema_meta, mirroring the existing
topic backfill, so existing databases pick the names up without a full
rescan: already-processed transcripts are re-read for attributionAgent
records only, leaving turns untouched so token totals cannot drift.

On a real 2123-file transcript tree this named all 134 dispatches, of
which only 40 were reachable from a parent agentType.
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.

1 participant