Skip to content

fix(server): provider subagent threads follow their parent into the archive - #330

Merged
bk-agent-01 merged 3 commits into
stagefrom
fix/bkt3-archive-orphan-subagents
Oct 19, 2026
Merged

bk-agent-01 merged 3 commits into
stagefrom
fix/bkt3-archive-orphan-subagents

Conversation

@bk-agent-01

@bk-agent-01 bk-agent-01 commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Archiving or deleting a thread never archives its provider-native subagent threads (Claude Task/Agent children: relationshipToParent: "subagent" and creationSource: "provider"). Clients hide these threads, but the server keeps them in every active-thread ("shell") read. On bkt3 on 2026-10-19, 944 of the 1,228 active shell rows were such threads. All of them were finished, and 469 had an archived or deleted parent. The shell query runs on the event loop, and those dead rows cost about 0.4 s of its 0.8 s.

Fix

A new fork-owned reactor, SubagentArchiveCascade.expbkt3.ts, plus one marked line in server.ts:

  • When a parent is archived or deleted, the reactor archives the parent's provider-native subagent threads. It skips a child whose subagent record is still live (pending, running or waiting) and archives it when that record finishes. delegate_task children and forks never move. A live cascade moves one child per 500 ms, so a parent with 63 subagents takes about 30 s.
  • When a parent is unarchived, it unarchives them again.
  • A startup pass archives the subagents of an archived, deleted or missing parent. bkt3 had 111 of them at 19:00 UTC (318 at 18:08, before a bulk clean-out).
    • The pass archives one thread every 2 seconds (about 11 minutes for 318), so the history exports that each archive triggers spread out. It re-checks each thread before it archives it.
    • It logs its progress every 50 threads, and one summary line at the end.
  • Commands: thread.archive / thread.unarchive with server:subagent-archive-cascade:*, server:subagent-unarchive-cascade:* and server:subagent-archive-startup:* command IDs.

Results of archiving (a review found the first one): the orphans' worktrees (56 on bkt3) become eligible for the slim auto-sweep (minimum age 14 days), as for any archived thread. On stage, the auto-sweep runs in remove mode after 7 days, so about 19 stage worktrees that only orphans protect can be removed within 6 hours after the startup pass (the dirty, untracked and unpushed gates still apply). On bkt3, the sweep runs in slim mode after 14 days, and the first of its 56 worktrees qualifies around 2026-10-26. The web app cannot open an archived thread directly, and the parent is archived too. Unarchiving the parent restores the threads.

Known, not fixed here: history export file names use the first 8 characters of the thread ID, and every provider subagent ID starts with thread:p, so some subagent exports overwrite each other. This existed before this PR (today's clean-out already hit it).

How I tested

  • New tests in SubagentArchiveCascade.expbkt3.test.ts (in-memory SQLite, real orchestrator, no sleeps):
    • archive and delete cascades, and the threads that must not move
    • unarchive brings the threads back
    • the startup pass archives orphans and leaves the threads it must not touch
    • the pacing: one orphan per pause, and one live child per 500 ms, with TestClock
    • a subagent that finishes after its parent was archived is archived then
  • I ran the startup and per-parent queries read-only against the live bkt3 database: 318 orphans in 14 ms, and 8 ms per parent on the largest project.
  • CI runs the tests and the typecheck. This host does not run them.
  • After the deploy, I will check the summary log line and measure the shell query time and row count.

🤖 Generated with Claude Code — Claude Opus 5.5 in T3 Code.

bk-agent-01 and others added 2 commits October 19, 2026 18:23
…rchive

Archiving or deleting a thread never archived the provider-native subagent
threads (Claude Task/Agent children and the like) mirrored under it. Every
client hides those threads, but they stayed in every active shell read: on
bkt3 on 2026-10-19, 318 of 1,144 active threads were finished subagents of an
archived parent.

A fork-owned reactor (SubagentArchiveCascade.expbkt3.ts) now:
- archives a parent's provider-native subagent children on thread.archived
  and thread.deleted, and unarchives them on thread.unarchived;
- once per start, after activation, archives every provider-native subagent
  whose parent is archived, deleted or missing, and logs the count.

delegate_task children, forks and children with an unfinished run are left
alone. Candidates come from one index-backed SQL read; commands go out four
at a time. server.ts gains one marked import and one marked layer line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every archive also starts a history export that runs git in the worktree, so
archiving bkt3's ~318 orphaned subagents at once would burst ~1,300-1,600 git
processes right after a deploy.

The startup pass now queues one orphan per pause (default 2 seconds, set with
`make({ startupPause })`), so the archives and their exports spread over ~11
minutes. A separate fiber does the pacing and feeds the same worker that runs
live cascades. Live cascades therefore run between orphans and are not held
for the whole pass, and nothing races them on the same child. Each orphan is
re-checked when its turn comes and skipped if its parent came back. The pass
logs progress once per 50 orphans, then one summary line.

Tests pass a zero pause, and a new test advances TestClock to show one archive
per pause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:L labels Oct 19, 2026
…live cascades

The "unfinished run" guard could never fire: a provider-native subagent
thread has no run (0 of 777 on bkt3). Its liveness lives in its parent's
subagent record (orchestration_v2_projection_subagents, one per child
thread). An archive now skips a child whose record is pending, running or
waiting, using the child_thread_id index. When a provider-native record
stops being live, its thread is re-checked and follows an archived parent,
so archiving a parent mid-turn no longer leaves its subagents behind until
the next restart. Unarchive no longer applies the guard.

Live cascades are now paced too: a live archive or unarchive waits until the
previous live move is `cascadePause` old (default 500 ms, an option to
`make`). A bulk clean-out of parents with 40-60 subagents each therefore no
longer bursts history exports. Startup orphans still come one per
`startupPause` from the pacing fiber, and they interleave with live work in
the shared worker. Each child is re-read just before it moves.

Tests seed a live subagent record instead of a run. New tests cover a
subagent that finishes after its parent was archived, and a cascade paced on
the TestClock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bk-agent-01
bk-agent-01 marked this pull request as ready for review October 19, 2026 19:24
@bk-agent-01
bk-agent-01 merged commit 76ca555 into stage Oct 19, 2026
25 of 27 checks passed
@bk-agent-01

Copy link
Copy Markdown
Collaborator Author

Merged into stage at head 63fd610c on the go of tushar in T3 session "Debug BKT3 Connection Drops" (answer "Stage only"; the bkmain promotion is his later call). All checks green on that head except "Transfer report artifact", which waits on a Blacksmith runner this repository does not have. Two fresh-context review rounds: no unsafe archive target (checked against the live bkt3 DB, 111 orphans); one P3 left as a follow-up (startup and live pacing do not share one pause). Known consequence, accepted: stage's remove-mode auto-sweep may remove about 19 stage worktrees that only orphan subagents protected.

@bk-agent-01

Copy link
Copy Markdown
Collaborator Author

Production check: this change went live through promotion #335 (bkmain 806b6390cc, 2026-10-20 13:41 UTC). The post-deploy check and the 24-hour check are on #335. The latest measurement with every fix of this goal live is in this #344 comment: 0.44 freezes per hour, against 1.43 before, and 0 stuck subagents.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 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