Subagent observability: hierarchy, live transitions, status badges, live tail#48
Open
JeanBaptisteRenard wants to merge 13 commits into
Open
Subagent observability: hierarchy, live transitions, status badges, live tail#48JeanBaptisteRenard wants to merge 13 commits into
JeanBaptisteRenard wants to merge 13 commits into
Conversation
Subagent transcripts written by Claude CLI live at
<folder>/<parentSessionId>/subagents/agent-<agentId>.jsonl alongside a
.meta.json sidecar holding { agentType, description }. Surface them as
first-class rows in session_cache, keyed by sessionId 'sub:<parent>:<agentId>'.
- adds parentSessionId, agentId, subagentType, description columns
- migration v4 clears the cache so a re-index repopulates everything
- adds idx_session_cache_parent for hierarchy lookups
- new query getCachedByParent + widened cacheGetByFolder
Adds helpers for the on-disk subagent layout: - enumerateSessionFiles(folderPath) walks top-level + <uuid>/subagents/*.jsonl + legacy <uuid>/*.jsonl fallback - subagentSessionId(parent, agentId) gives the synthetic 'sub:<p>:<a>' id - resolveJsonlPath(projectsDir, row) reconstructs the absolute path - readSubagentMeta reads the .meta.json sidecar readSessionFile now accepts opts.parentSessionId. When set, it requires isSidechain on at least one entry (defensive guard), reads the sidecar for agentType/description, falls back to filename for agentId if absent in entries. Adds 10 unit tests covering both branches, the sidecar guard, the legacy fallback path, and the synthetic-id format.
Both scanners (synchronous refreshFolder + worker scan-projects) now use enumerateSessionFiles instead of a flat top-level readdir, so subagent transcripts get cached as first-class rows with parentSessionId set. FTS indexing inherits this for free: subagent summaries land in search_fts the same way top-level sessions do, with type='session' and folder=<parent>. Full-text search now finds anything an Agent call discussed.
Clicking an Agent tool_use block in the JSONL viewer now loads the matching subagent transcript and renders it nested below the block. Re-click collapses without refetching. - main.js: new IPC read-subagent-jsonl(parentSessionId, agentId) and list-subagents(parentSessionId) - preload.js: window.api.readSubagentJsonl / listSubagents - jsonl-viewer.js: clickable Agent block, caret indicator, recursive nested render. Identical (description, subagentType) pairs are disambiguated by occurrence ordinal so parallel fanout calls each open their own transcript. - style.css: subtle hover + left-border indent for nested transcripts
Runs `npm ci && npm test` on ubuntu-latest against Node 20 and 22. Triggers on pull requests targeting main and on direct pushes to main.
2286812 to
e51061d
Compare
Migrations that clear session_cache (v2, v3, v4) leave the cache empty on first launch. The previous get-projects handler returned [] immediately and fire-and-forgot the worker scan, relying on a later 'projects-changed' IPC to trigger a reload — but app.js only reloads on that event when the user happens to be on the Sessions tab. If they were on another tab (or hadn't navigated to Sessions yet), the list stayed empty until they manually switched tabs. Make populateCacheViaWorker return a Promise that resolves when the scan finishes. Concurrent callers share the same Promise. get-projects awaits that Promise when the cache is empty, so the response carries the freshly populated cache and the renderer paints immediately.
e51061d to
46b3f1f
Compare
buildProjectsFromCache was projecting only the pre-PR#1 columns onto the session object sent to the renderer. The renderer's hierarchical sidebar (introduced in PR#2) reads parentSessionId/agentId/subagentType/description from each session and silently flattens to top-level when they're missing — which is what happened: all subagents appeared as siblings of their parent instead of nested under it. Add the four subagent columns to the projection. No DB or wire change.
46b3f1f to
24c2241
Compare
24c2241 to
4ac1d2b
Compare
readSessionFile JSON.parse'd every line of a session file in a single outer try/catch. If a Claude CLI session was actively writing the file while the worker scan read it, one mid-write line could throw and invalidate the ENTIRE file's session row. With many parallel live sessions this manifested as 'most projects show no sessions after a fresh index'. - Per-line try/catch inside readSessionFile: skip malformed lines, keep parsing the rest. - Per-file try/catch in workers/scan-projects.js: defensive belt and braces so one unparseable file can't abort an entire folder scan.
4ac1d2b to
e893501
Compare
resolveWorktreePath was defined and exported in the file but never actually called from deriveProjectPath. As a result every worktree under <project>/.claude/worktrees/<name>/ appeared as a separate project group in the sidebar instead of being grouped under its parent project. Wire the call in both branches of deriveProjectPath (direct .jsonl path and subdirectory path) and export resolveWorktreePath for other callers.
session-transitions.js now tracks per-active-session knownSubagents and emits two new IPC events: - subagent-spawned (parentSessionId, agentId, subagentType, description) when a new agent-*.jsonl file appears under <sid>/subagents/ - subagent-completed (parentSessionId, agentId) when an existing file's mtime has been stable for >30s Adds two IPCs for read-only live tailing: - start-subagent-watch (parent, agentId) -> watchId - stop-subagent-watch (watchId) The watcher streams new JSONL entries via subagent-watch-event so the renderer can append them to an open inline-expanded subagent transcript without polling.
- sidebar.js: subagents are no longer flat siblings of their parent. Each top-level row now shows a 'N subagents' affordance with a disclosure caret; expanded children render indented with a subagentType pill, description as label, and persist expansion state per parent in localStorage. Orphan subagents (parent absent from cache) hoist to a 'Orphan subagents' group. - grid-view.js: each active session card now shows a stack of small pills for currently-running subagents, color-coded by subagentType, capped at 5 with a '+N more' overflow. Listens on onSubagentSpawned/onSubagentCompleted. - jsonl-viewer.js: when an inline-expanded subagent block represents a still- running agent, start an fs.watch via the new IPC and append streamed entries; show a small '● live' indicator until completion. Stops the watch on collapse or subagent-completed. - style.css: minimal styles for new sidebar/grid/live elements, matching existing palette and font weights.
When Switchboard attaches to a session that already has many subagent files on disk (e.g. a long-running session with 100+ Agent calls), the first walk of detectSubagentTransitions treated every existing file as a 'first sighting' and emitted subagent-spawned for each. 30s later it emitted subagent-completed for each. Hundreds of IPC events back to back froze the renderer UI. Distinguish bootstrap (first walk for this session) from steady-state. On bootstrap, record every existing file in knownSubagents silently: - files modified in the last 60s stay in the active lifecycle (could be mid-run, will eventually fire subagent-completed) - older files are marked completed immediately with no IPC Only files that appear AFTER the bootstrap walk fire subagent-spawned.
e893501 to
a6210a6
Compare
…PC, unwatchFile) Three correctness bugs discovered while reviewing PR#48 — all small, all prevented subagent observability features from actually working on the intended path: 1. public/sidebar.js:438 — `subagentIndex` was missing from the `processProjectSessions` result destructure. Reference triggered a ReferenceError that aborted the project loop, leaving the sidebar blank when any project had subagents. 2. public/grid-view.js — `onSubagentSpawned` / `onSubagentCompleted` listeners were registered with `(event, data) => ...` but preload.js exposes them as `cb(payload)` (single arg). `data` was always undefined so grid card pills never updated on spawn/complete. 3. main.js — `fs.unwatchFile(entry.filePath)` was called without the listener arg, which removes ALL listeners on that path. Store the listener in subagentWatchers and pass it through so two concurrent watches on the same file can stop independently.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Builds on #47 (subagent indexing + inline expansion). This PR adds observability of in-flight subagents — useful when Claude fans out 5-10 parallel agents and you want to monitor them from one screen.
session-transitions.js. New per-sessionknownSubagentsmap; new IPC eventssubagent-spawnedandsubagent-completed.subagentTypeas a pill anddescriptionas label. Expansion state persists inlocalStorage. Orphan subagents (parent absent) hoist to a dedicated group at the bottom.+N moreoverflow.fs.watch-backed stream appends new JSONL entries as they arrive. Small● liveindicator untilsubagent-completedfires.Dependencies
Stacked on #47. Merge that first; this PR's diff against
mainwill then be just the observability work. While #47 is open this branch's base showsfeat/subagent-support.Test plan
node --checkpasses on all modified filesnpm test— existing PR#1 tests still pass (no new tests added; this PR is integration-heavy)● liveindicator clears on completionsubagents/dir, confirm they appear in the "Orphan" group rather than disappearingNotes for review
knownSubagentslives on the active session object and is GC'd when the PTY exits — no separate cleanup neededstop-subagent-watchor main-window closetypeof window.api.onSubagentSpawned === 'function') so the renderer doesn't crash if preload changes again later