Skip to content

Keep the turn activity tracker steady while subagents run - #157

Merged
badcuban merged 2 commits into
mainfrom
threadlines/fix-subagent-activity-flicker
Aug 14, 2026
Merged

Keep the turn activity tracker steady while subagents run#157
badcuban merged 2 commits into
mainfrom
threadlines/fix-subagent-activity-flicker

Conversation

@badcuban

Copy link
Copy Markdown
Collaborator

Fixes two long-standing issues with the inline subagent activity bar in the main chat.

Flickering activity bar

A running agent's streamed commentary becomes a subagent-live timeline entry. It renders nothing, but its createdAt moves forward on every stream update, and it still broke work-group contiguity in deriveMessagesTimelineRows. The result: as the agent streamed, the entry hopped between the middle and the tail of the turn's work entries, splitting and re-merging the activity groups. Whenever the groups merged into one live group, the receipt (the "Activity · N actions · 2 subagents" row with the tracker) vanished, because the live group only rendered the step spine. That is the flicker.

Two changes:

  • subagent-live entries are now filtered out before grouping (alongside subagent-attributed work entries), so group boundaries no longer depend on a moving timestamp.
  • A live work group that owns the turn's tracker now renders the collapsed activity receipt above its live spine, so the tracker stays visible for the whole life of a delegating turn instead of only appearing on settled groups.

Model and reasoning effort intermittently missing

#155 restricted parent-turn model/effort inheritance to records seen for the first time (previous === undefined). But since the durable subagent roster started seeding a record for every agent before activity rows are folded in (f658a57), previous is always defined, so the inheritance floor was dead. Any collab spawn that did not state an explicit model (Codex spawn_agent without model args, Claude Task without an override) showed a blank model, and whether the bar showed model/effort depended on whether the orchestrator happened to pass explicit args.

The fix makes "native spawn omission is deliberate" a sticky per-record fact (nativeSpawn on the internal record) instead of inferring it from record freshness:

  • Native subAgentActivity spawns still never inherit, including through later wait/sendInput/closeAgent items (preserves Preserve native subagent model omissions #155).
  • Collab and Task spawns now inherit the turn's dispatched model and effort even when the roster seeded their record first.

Tests

  • New unit test: live agent commentary interleaved with the turn's steps keeps one work group.
  • New unit test: a roster-seeded collab spawn without a model inherits the turn's dispatched settings (regression for the Preserve native subagent model omissions #155 x roster-seed interaction). All four existing Preserve native subagent model omissions #155 semantics tests still pass.
  • New browser test: the tracker stays visible while the turn's work group is live.

vp fmt, vp lint, vp run typecheck green. Full web unit suite: 2194 passed. Full browser suite: 465/466, with the one failure an unrelated FileViewerPierre flake that passes in isolation.

Two fixes for the inline subagent activity bar in the conversation:

Flicker: a running agent's streamed commentary is a timeline entry that
renders nothing, but its timestamp moves with every update. It could
split and re-merge the turn's work groups as the agent streamed, and the
live group never rendered the activity receipt, so the tracker blinked
in and out mid-turn. Live commentary entries now stay out of grouping
entirely, and a live work group that owns the turn's tracker renders the
receipt above its spine so the bar survives group churn.

Missing model/effort: the durable roster seeds a record for every agent
before lifecycle rows fold in, so the "only inherit for a brand-new
record" guard from #155 never fired and a collab spawn that stated no
model lost the parent turn's dispatched selection. Native-spawn omission
is now a sticky per-record fact instead: native subAgentActivity spawns
still never inherit, while collab and Task spawns inherit the turn's
model and effort even when the roster seeded their record first.
@vercel

vercel Bot commented Aug 14, 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 14, 2026 2:45pm

Request Review

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:M labels Aug 14, 2026
@badcuban
badcuban merged commit 7718a47 into main Aug 14, 2026
15 checks passed
@badcuban
badcuban deleted the threadlines/fix-subagent-activity-flicker branch August 14, 2026 14:53
badcuban added a commit that referenced this pull request Aug 15, 2026
…tab (#160)

* Keep a subagent's work out of the inline feed and surface the Agents tab

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.

* Sanitize subagent result text without polynomial backtracking

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M 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.

1 participant