Repository navigation
Fix: Label workflow subagents in the agentop timeline #1757
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import ( | |
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "maps" | ||
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
|
|
@@ -460,6 +461,14 @@ func ReadSessions(configDir string, since map[string]SessionMetadata) (map[strin | |
| // converging. | ||
| won[id] = st.ModTime() | ||
| delete(out, id) | ||
| // The subagent labels are re-read anyway. A workflow (Claude Code's Workflow | ||
| // tool) starts its agents without a write to the main transcript, so the | ||
| // mtime above does not move while new agents start. The meta files are small. | ||
| if subs := readSubagents(filepath.Join(projDir, id, "subagents")); !maps.Equal(subs, since[id].Subagents) { | ||
| kept := since[id] | ||
| kept.Subagents = subs | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: After this change, a skipped session can appear in the returned map. The |
||
| out[id] = kept | ||
| } | ||
| skipped++ | ||
| continue | ||
| } | ||
|
|
@@ -479,11 +488,8 @@ func ReadSessions(configDir string, since map[string]SessionMetadata) (map[strin | |
| LogFile: path, | ||
| LogModTime: st.ModTime(), | ||
| // The subagents' labels live in a sibling DIRECTORY of the transcript | ||
| // (<id>/subagents), whose mtime the skip above knows nothing about. That | ||
| // is fine, because a session only gains a subagent by appending the Agent | ||
| // tool call that spawned it to the transcript — which moves the | ||
| // transcript's mtime and re-parses the session, meta files and all. A | ||
| // skipped session keeps the Subagents its `since` entry already carried. | ||
| // (<id>/subagents), whose mtime the skip above knows nothing about, so the | ||
| // skip re-reads them on its own. | ||
| Subagents: readSubagents(filepath.Join(projDir, id, "subagents")), | ||
| } | ||
| } | ||
|
|
@@ -492,19 +498,31 @@ func ReadSessions(configDir string, since map[string]SessionMetadata) (map[strin | |
| } | ||
|
|
||
| // readSubagents reads one session's subagents/agent-<id>.meta.json files into a map keyed | ||
| // by the bare id. Nil when the directory is missing or holds nothing harvestable — a | ||
| // session that never spawned a subagent, which is most of them, must not carry an empty | ||
| // map the JSON would then spell out. | ||
| // by the bare id. It also reads subagents/workflows/<run>/agent-<id>.meta.json, where | ||
| // Claude Code puts the agents that its Workflow tool starts. Nil when the directory is | ||
| // missing or holds nothing harvestable — a session that never spawned a subagent, which | ||
| // is most of them, must not carry an empty map the JSON would then spell out. | ||
| // | ||
| // Best-effort per file, for the same reason titleFromTranscript is: one unparseable meta | ||
| // file must not cost the other labels. The transcripts sitting beside the meta files are | ||
| // deliberately not read — see SessionMetadata.Subagents. | ||
| func readSubagents(dir string) map[string]SubagentInfo { | ||
| out := readSubagentDir(dir, nil) | ||
| runs, _ := os.ReadDir(filepath.Join(dir, "workflows")) | ||
| for _, r := range runs { | ||
| if r.IsDir() { | ||
| out = readSubagentDir(filepath.Join(dir, "workflows", r.Name()), out) | ||
| } | ||
| } | ||
| return out | ||
| } | ||
|
|
||
| // readSubagentDir adds the agent-<id>.meta.json files in dir to out, and returns out. | ||
| func readSubagentDir(dir string, out map[string]SubagentInfo) map[string]SubagentInfo { | ||
| entries, err := os.ReadDir(dir) | ||
| if err != nil { | ||
| return nil | ||
| return out | ||
| } | ||
| var out map[string]SubagentInfo | ||
| for _, e := range entries { | ||
| name := e.Name() | ||
| if !strings.HasPrefix(name, "agent-") || !strings.HasSuffix(name, ".meta.json") { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: The gate keys on the description rather than on whether the harvest found the agent at all. A meta file that has
agentTypebut nodescriptionhas aSubagents[id]entry, so further harvests can't add anything for it. Even so,harvestedLabelreturns""for it. The row stayssubagent <id>, and on a live session this asks for a harvest every 15 s for as long as the pane is open. That's the same cost as the teammate "known limit", but here it comes from an agent the harvest did find.subagentLabelinevents_blocks.goalready falls back from Description to AgentType. Two changes would fix this: have the gate test key presence (_, ok := m.sessionsData[s].Subagents[a.ID]), and have the label use the same Description → AgentType fallback, so both panes name such an agent the same way. Not blocking: the 15 s floor already bounds the cost.