Skip to content

Keep a subagent's work out of the inline feed and surface the Agents tab - #160

Merged
badcuban merged 3 commits into
mainfrom
subagent-inline-activity
Aug 15, 2026
Merged

Keep a subagent's work out of the inline feed and surface the Agents tab#160
badcuban merged 3 commits into
mainfrom
subagent-inline-activity

Conversation

@badcuban

Copy link
Copy Markdown
Collaborator

A background Claude agent's activity flooded the parent conversation ("Activity · 128 actions" with 110+ leaked rows), the inline agent tracker never rendered for turnless background stretches, the Agents tab gave no signal that a delegation had started, and the durable subagent roster (projection_thread_subagents) had never contained a single row.

Conversation feed

  • deriveWorkLogEntries no longer strips activityKind, which had silently disabled the agent-lifecycle parking from Keep the turn activity tracker steady while subagents run #157 in production. A subagent's task.progress/task.completed rows now park as invisible anchors; the Agents rail owns the detail.
  • Background tasks an agent starts inside its own conversation (e.g. its vp test runs) are attributed to the spawning agent: the Claude adapter records every tool_use id seen in forwarded subagent messages and stamps task events with ownerAgentToolUseId (new optional contract field), which the web parks alongside the agent's task stream. Historical rows from before this change cannot be re-attributed and may still render; that fades as threads wrap.
  • Turnless work groups carry trackerAgentSpawnIds and selectTurnAgents gained a spawn-call fallback, so the group at the conversation tail shows tracker bars + the live status line while a background agent works. An all-delegation group's receipt reads "Agent working" / "Agents working" / "Agent activity" instead of claiming zero actions.

Agents tab auto-open

A fresh idle→running transition surfaces the Agents tab: focused when the sidebar is hidden or on the launcher, joined in the background (live node only) when another tab has focus. The edge detector (advanceAgentsAutoOpenEdge, unit-tested) only believes idleness after the thread's detail snapshot has synced — the chat column publishes a hydrated flag with its agent state — so loading a thread whose agent is already running never yanks the panel open. Once per batch, sheet layouts opt out, closing mid-batch is respected.

Durable roster

The projection only recognized Codex-shaped collab items (data.item), so Claude agents never produced roster rows. The Claude item shaping moved from web session-logic to @threadlines/shared/claudeSubagentActivity; the server projection uses it too, links Claude transcript task ids, settles agents from task.completed (including restart-synthesized completions carrying only a taskId), and maps the shaper's errored state to failed. Migration 048 backfills the roster by replaying each thread's roster-moving activities through the same fold — per thread, in rowid (append) order so same-timestamp lifecycle rows cannot resurrect a settled agent.

Verification

  • Unit: new tests for the projection fold (Claude + Codex shapes, launch acknowledgments, errored, taskId-only settles, transcript linking), the timeline parking through the real derivation, the tracker spawn fallback, the auto-open state machine and edge detector, and adapter task-ownership attribution. Full server/web/browser suites green.
  • Independent review (gpt-5.6-sol): four findings (migration ordering, unbounded migration memory, unrecognized errored, taskId-only settles) — all fixed.
  • Sandbox (throwaway server+web+Playwright against a snapshot of real data): migration backfilled 27 agents in ~6s; the flood thread renders clean with the tracker + live status at the tail; tracker click drills into the Agents panel; main-agent receipts unaffected; ordinary threads render clean; load/reload of a running-agent thread no longer auto-opens the panel.

A background Claude agent's activity flooded the parent conversation: every
per-tool task.progress tick rendered as its own inline row ("Activity - 128
actions" with 110+ leaked entries), while the agent tracker that should
summarize the delegation never showed for turnless background stretches.

Conversation feed:
- deriveWorkLogEntries no longer strips activityKind from emitted entries,
  which had made the timeline's agent-lifecycle parking (isAgentLifecycleEntry)
  dead code in production - the exact check #157 shipped.
- Background tasks an agent starts inside its own conversation (e.g. its test
  runs) are now attributed to the spawning agent: the Claude adapter records
  every tool_use id seen in forwarded subagent messages and stamps task events
  with ownerAgentToolUseId (new optional field on the task payload contracts),
  which the web parks out of the feed alongside the agent's own task stream.
- Work groups with no turn to key on carry trackerAgentSpawnIds, and
  selectTurnAgents gained a spawn-call fallback, so a turnless background-agent
  group at the conversation tail shows the tracker bars and live status line
  instead of nothing. An all-delegation group's receipt reads "Agent working" /
  "Agents working" / "Agent activity" instead of claiming zero actions.

Agents tab:
- A fresh idle-to-running agent transition auto-surfaces the Agents tab:
  focused when the sidebar is hidden or on the launcher, joined in the
  background (live node only) when another tab has focus. The edge detector
  (advanceAgentsAutoOpenEdge) only believes idleness after the thread's detail
  snapshot has synced - the chat column now publishes a hydrated flag with its
  agent state - so loading or reloading a thread whose agent is already running
  never yanks the panel open. Once per batch, never on sheet layouts, and
  closing the tab mid-batch is respected.

Durable roster:
- projection_thread_subagents was empty for every thread: the projection only
  recognized Codex-shaped collab items (data.item). The Claude item shaping
  moved from web session-logic into @threadlines/shared/claudeSubagentActivity
  and the server projection now uses it too, links Claude transcript task ids,
  settles agents from task.completed (including restart-synthesized completions
  that carry only a taskId), and maps the shaper's "errored" state to failed.
- Migration 048 backfills the roster by replaying each thread's roster-moving
  activities through the same fold, in rowid (append) order so same-timestamp
  lifecycle rows cannot resurrect a settled agent, one thread at a time.

Verified end to end against a snapshot of real data: migration backfilled 27
agents in ~6s, the flood thread renders clean with the tracker live at the
tail, and load/reload of a running-agent thread no longer auto-opens the panel.
@vercel

vercel Bot commented Aug 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
threadlines Skipped Skipped Aug 15, 2026 9:12pm

Request Review

@github-actions github-actions Bot added size:XL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Aug 15, 2026
Comment thread packages/shared/src/claudeSubagentActivity.ts Fixed
CodeQL flagged the moved sanitizer's regexes (js/polynomial-redos): the
trailing <usage> stripper and the SendMessage continuation-footer stripper
both backtracked polynomially, and agent output is provider-influenced
input. The usage stripper is now index arithmetic, and the footer pattern
only ever runs anchored against a bounded slice from the last "agentId:".
Covered by direct tests for the shared module, including an adversarial
repetition case that would time out on the quadratic implementation.
@badcuban
badcuban merged commit 7c4e171 into main Aug 15, 2026
15 checks passed
@badcuban
badcuban deleted the subagent-inline-activity branch August 15, 2026 21:22
badcuban added a commit that referenced this pull request Aug 16, 2026
…, detection shielded

Fixes for three issues observed live after #160:

- Promoted codex exec runs never appeared in the panel. The server narrates
  a promoted run entirely through subagent.metadata activities, and the only
  thing the web did with that kind was filter it out of the work log - the
  durable roster row it fed only reached the client on a full detail
  snapshot, i.e. after a reload. The shared subagent record fold now applies
  metadata activities (spawn with callId, the later agent-id link, completion
  with the result body), so a promoted run shows up while it runs and
  settles in place. Tests use the three payloads a live run projected.
- "Detected agent process" rows surfaced the session's own transient
  children: any pid or command mentioned in chat prose (pgrep output, a
  quoted setup script) could match the provider's shell wrappers and search
  helpers, offering stop buttons for the harness's own processes.
  Process-only detection now shields the entire subtree under any live
  provider CLI process - that work is tracked, not lost - while genuinely
  orphaned processes (reparented to init/launchd) stay detectable.
- Background command runs (CI watchers, gate runs) crowded the top of the
  Agents tree while actual agents sat under "Earlier". The view now splits
  them into a Commands section below the agents, keeping their rows and stop
  handles. Consistently, a running command no longer counts as a running
  agent anywhere: not in the tab's live dot, not in the auto-open trigger,
  not in the launcher counts (which now name background commands as what
  they are).
badcuban added a commit that referenced this pull request Aug 17, 2026
* Harden the subagent roster fold and close review gaps from the live demo

Follow-ups from two Opus review agents and a sol second-opinion run against
the merged #160 (which also served as the live verify of the new agent UX):

- The roster fold could emit two rows for one agent when a spawn's pending
  placeholder and an id-keyed row (learned from a later wait item) were
  bridged by a patch carrying both keys. The persisted table is unique on
  id/agentThreadId/spawnCallId per thread, so the write fails - and in
  migration 048 that failure aborted the transaction and left the server
  unbootable on every start. Patches now merge ALL matching rows into one.
- Migration 048: json_valid() guards the candidate filter (json_extract
  raises on malformed payloads rather than returning NULL), orphaned roster
  rows for threads with no roster-moving activities are cleaned up, a thread
  whose replay fails is logged and skipped instead of failing the migration,
  and the rowid-ordering docblock now notes the thread.reverted caveat. New
  migration test covers both provider shapes, the collision, a malformed
  payload row, and orphan cleanup.
- An agent's owned background tasks (ownerAgentToolUseId rows) were parked
  from the conversation but invisible in the Agents rail; the drill-in work
  view now includes them alongside sourceAgentThreadId rows.
- An all-anchor work group with no resolvable tracker rendered null inside
  a padded timeline row, leaving phantom 16px gaps; the row wrapper now
  collapses its padding when the section renders nothing.
- The <usage> stripper in the shared sanitizer re-lowercased the string per
  stacked block (quadratic on hostile tails); it now lowercases once and
  walks an index backward, with a stacked-blocks regression test.

* Make the Agents tab about agents: commands sectioned, promotions live, detection shielded

Fixes for three issues observed live after #160:

- Promoted codex exec runs never appeared in the panel. The server narrates
  a promoted run entirely through subagent.metadata activities, and the only
  thing the web did with that kind was filter it out of the work log - the
  durable roster row it fed only reached the client on a full detail
  snapshot, i.e. after a reload. The shared subagent record fold now applies
  metadata activities (spawn with callId, the later agent-id link, completion
  with the result body), so a promoted run shows up while it runs and
  settles in place. Tests use the three payloads a live run projected.
- "Detected agent process" rows surfaced the session's own transient
  children: any pid or command mentioned in chat prose (pgrep output, a
  quoted setup script) could match the provider's shell wrappers and search
  helpers, offering stop buttons for the harness's own processes.
  Process-only detection now shields the entire subtree under any live
  provider CLI process - that work is tracked, not lost - while genuinely
  orphaned processes (reparented to init/launchd) stay detectable.
- Background command runs (CI watchers, gate runs) crowded the top of the
  Agents tree while actual agents sat under "Earlier". The view now splits
  them into a Commands section below the agents, keeping their rows and stop
  handles. Consistently, a running command no longer counts as a running
  agent anywhere: not in the tab's live dot, not in the auto-open trigger,
  not in the launcher counts (which now name background commands as what
  they are).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants