authorgravatar for 91240327+shivamhwp@users.noreply.github.comshivam <91240327+shivamhwp@users.noreply.github.com> 2026-10-14 07:32:09+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-14 07:32:09+05:30
log109aaae6cd251e1d3aa595624209758a164d8b33
tree8a051494bbbb3c15a5c5bc95252045e9c050786c
parenta4e2539483495d000a30f5f3c01aadb36f13dcec
signature Signed by PGP key B5690EEEBB952194

fix(client): "Subagent needs input" no longer sticks after the subagent finishes (#18996)

Co-authored-by: PR Batch Tester <agent@local.test> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

2 files changed, 64 insertions(+), 1 deletions(-)

packages/client-runtime/src/state/threadShell.test.ts+49-1
......@@ -1,6 +1,7 @@
11import {
22 EnvironmentId,
33 ProjectId,
4 RuntimeRequestId,
45 ThreadId,
56 type OrchestrationV2ShellSnapshot,
67} from "@t3tools/contracts";
......@@ -9,7 +10,7 @@ import { Atom, AtomRegistry } from "effect/reactivity";
910import { describe, expect, it } from "vite-plus/test";
1011
1112import { PrimaryConnectionTarget } from "../connection/model.ts";
12import { v2ShellSnapshot, v2ThreadShell } from "./orchestrationV2TestFixtures.ts";
13import { v2Now, v2ShellSnapshot, v2ThreadShell } from "./orchestrationV2TestFixtures.ts";
1314import { applyShellStreamEvent } from "./shellReducer.ts";
1415import { createEnvironmentThreadShellAtoms } from "./threadShell.ts";
1516
......@@ -103,6 +104,53 @@ describe("v2 thread shell lists", () => {
103104 registry.dispose();
104105 });
105106
107 it("counts a subagent question on its parent only while the subagent runs", () => {
108 const { registry, threads, snapshotAtom } = makeHarness();
109 const root = v2ThreadShell;
110 const child = {
111 ...root,
112 id: ThreadId.make("child"),
113 creationSource: "mcp" as const,
114 status: "running" as const,
115 activityRunStatus: "running" as const,
116 lineage: {
117 ...root.lineage,
118 parentThreadId: root.id,
119 relationshipToParent: "subagent" as const,
120 },
121 pendingRuntimeRequest: {
122 id: RuntimeRequestId.make("question"),
123 kind: "user_input" as const,
124 createdAt: v2Now,
125 },
126 };
127 const inputs = threads.childThreadInputsAtom({ environmentId, threadId: root.id });
128 const dispose = registry.mount(inputs);
129 registry.set(snapshotAtom(environmentId), { ...v2ShellSnapshot, threads: [root, child] });
130 expect(registry.get(inputs).map((thread) => thread.id)).toEqual([child.id]);
131 // A follow-up queued behind the blocked run leaves the question live.
132 registry.set(snapshotAtom(environmentId), {
133 ...v2ShellSnapshot,
134 threads: [root, { ...child, status: "queued" as const }],
135 });
136 expect(registry.get(inputs).map((thread) => thread.id)).toEqual([child.id]);
137 // An async question stays pending after the run that asked it completes.
138 registry.set(snapshotAtom(environmentId), {
139 ...v2ShellSnapshot,
140 threads: [root, { ...child, status: "completed" as const, activityRunStatus: null }],
141 });
142 expect(registry.get(inputs)).toEqual([]);
143 // Older servers omit the activity run, so the latest run's status decides.
144 const { activityRunStatus: _omitted, ...legacyChild } = child;
145 registry.set(snapshotAtom(environmentId), {
146 ...v2ShellSnapshot,
147 threads: [root, { ...legacyChild, status: "completed" as const }],
148 });
149 expect(registry.get(inputs)).toEqual([]);
150 dispose();
151 registry.dispose();
152 });
153
106154 it("leaves switched-off environments out of navigation", () => {
107155 const { registry, threads, catalogValueAtom } = makeHarness([
108156 environmentId,
packages/client-runtime/src/state/threadShell.ts+15
......@@ -30,6 +30,20 @@ const EMPTY_THREAD_REFS_BY_PROJECT: ReadonlyMap<
3030 ReadonlyArray<ScopedThreadRef>
3131> = new Map();
3232
33// An async question outlives the subagent's run, but once that run ends the
34// parent is no longer waiting on the answer, so it stops counting as input.
35// The activity run stays set when a follow-up is queued behind a blocked run;
36// older servers omit it, so fall back to the latest run's status.
37const isChildRunLive = (
38 thread: Pick<OrchestrationV2ThreadShell, "activityRunStatus" | "status">,
39) =>
40 thread.activityRunStatus !== undefined
41 ? thread.activityRunStatus !== null
42 : thread.status === "preparing" ||
43 thread.status === "starting" ||
44 thread.status === "running" ||
45 thread.status === "waiting";
46
3347export function createEnvironmentThreadShellAtoms(input: {
3448 readonly catalogValueAtom: Atom.Atom<EnvironmentCatalogState>;
3549 readonly snapshotAtom: (
......@@ -163,6 +177,7 @@ export function createEnvironmentThreadShellAtoms(input: {
163177 pending.push(child.id);
164178 if (
165179 child.pendingRuntimeRequest?.kind === "user_input" &&
180 isChildRunLive(child) &&
166181 !isProviderNativeSubagentThread(child)
167182 ) {
168183 next.push(scopedThread(ref.environmentId, child));