From 8faebefda4867b02f8d44ccd407ba4c65c3734c8 Mon Sep 17 00:00:00 2001 From: dan Date: Sat, 17 Oct 2026 14:50:54 -0700 Subject: [PATCH 1/2] fix(client): a subagent question from a finished run no longer flags the parent An async user_input question stays pending after the subagent run that asked it ends. When the subagent starts a later run, the client counted the old question as live again, so the parent showed "Subagent needs input" and left Working for a question nobody was waiting on. Only count a question created at or after the activity run started its work. --- .../src/state/threadShell.test.ts | 49 +++++++++++++++++++ .../client-runtime/src/state/threadShell.ts | 12 ++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/client-runtime/src/state/threadShell.test.ts b/packages/client-runtime/src/state/threadShell.test.ts index 413a5c2b3f575..a14b4979f7293 100644 --- a/packages/client-runtime/src/state/threadShell.test.ts +++ b/packages/client-runtime/src/state/threadShell.test.ts @@ -152,6 +152,55 @@ describe("v2 thread shell lists", () => { registry.dispose(); }); + it("ignores a subagent question left over from a run that already finished", () => { + const { registry, threads, snapshotAtom } = makeHarness(); + const child = { + ...v2ThreadShell, + id: ThreadId.make("child"), + creationSource: "mcp" as const, + status: "running" as const, + activityRunStatus: "running" as const, + activityRunStartedAt: DateTime.add(v2Now, { minutes: 5 }), + lineage: { + ...v2ThreadShell.lineage, + parentThreadId: v2ThreadShell.id, + relationshipToParent: "subagent" as const, + }, + pendingRuntimeRequest: { + id: RuntimeRequestId.make("question"), + kind: "user_input" as const, + createdAt: v2Now, + }, + }; + const root = { + ...v2ThreadShell, + latestRunId: RunId.make("root-run"), + status: "completed" as const, + pendingBackgroundTasks: [ + { taskId: "lint", kind: "subagent" as const, childThreadId: child.id }, + ], + }; + const parent = threads.threadShellAtom({ environmentId, threadId: root.id }); + const inputs = threads.childThreadInputsAtom({ environmentId, threadId: root.id }); + const disposeParent = registry.mount(parent); + const disposeInputs = registry.mount(inputs); + registry.set(snapshotAtom(environmentId), { ...v2ShellSnapshot, threads: [root, child] }); + expect(registry.get(inputs)).toEqual([]); + expect(registry.get(parent)?.pendingSubagentRequest).toBeNull(); + expect(isThreadWorking(registry.get(parent)!)).toBe(true); + + registry.set(snapshotAtom(environmentId), { + ...v2ShellSnapshot, + threads: [root, { ...child, activityRunStartedAt: DateTime.subtract(v2Now, { minutes: 5 }) }], + }); + expect(registry.get(inputs).map((thread) => thread.id)).toEqual([child.id]); + expect(registry.get(parent)?.pendingSubagentRequest).toBe("user_input"); + expect(isThreadWorking(registry.get(parent)!)).toBe(false); + disposeInputs(); + disposeParent(); + registry.dispose(); + }); + it("brings a parent back from Working while a nested subagent waits for approval", () => { const { registry, threads, snapshotAtom } = makeHarness(); const child = { diff --git a/packages/client-runtime/src/state/threadShell.ts b/packages/client-runtime/src/state/threadShell.ts index a462638144bf0..32e910e47c666 100644 --- a/packages/client-runtime/src/state/threadShell.ts +++ b/packages/client-runtime/src/state/threadShell.ts @@ -7,6 +7,7 @@ import type { ThreadId, } from "@t3tools/contracts"; import { isProviderNativeSubagentThread, OrchestrationV2ThreadShell } from "@t3tools/contracts"; +import * as DateTime from "effect/DateTime"; import * as Schema from "effect/Schema"; import { Atom } from "effect/reactivity"; @@ -45,6 +46,14 @@ const isChildRunLive = ( thread.status === "running" || thread.status === "waiting"; +// An async question asked before the activity run's work started belongs to a +// run the subagent already finished, so the parent is no longer waiting on it. +const isStaleQuestion = (thread: OrchestrationV2ThreadShell) => + thread.pendingRuntimeRequest?.kind === "user_input" && + thread.activityRunStartedAt != null && + DateTime.toEpochMillis(thread.pendingRuntimeRequest.createdAt) < + DateTime.toEpochMillis(thread.activityRunStartedAt); + /** Provider-native subagents ask on their parent, so they are skipped. */ function subagentRequest(thread: OrchestrationV2ThreadShell): PendingSubagentRequest | null { @@ -54,7 +63,8 @@ function subagentRequest(thread: OrchestrationV2ThreadShell): PendingSubagentReq kind === "auth_refresh" || thread.lineage.relationshipToParent !== "subagent" || isProviderNativeSubagentThread(thread) || - !isChildRunLive(thread) + !isChildRunLive(thread) || + isStaleQuestion(thread) ) { return null; } From c111c22a077b4e422993a99e2fb9294d28054d05 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:00:11 -0700 Subject: [PATCH 2/2] fix(client): clarify stale subagent question filtering --- packages/client-runtime/src/state/threadShell.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/client-runtime/src/state/threadShell.ts b/packages/client-runtime/src/state/threadShell.ts index 32e910e47c666..242d3956b3288 100644 --- a/packages/client-runtime/src/state/threadShell.ts +++ b/packages/client-runtime/src/state/threadShell.ts @@ -46,9 +46,11 @@ const isChildRunLive = ( thread.status === "running" || thread.status === "waiting"; -// An async question asked before the activity run's work started belongs to a -// run the subagent already finished, so the parent is no longer waiting on it. -const isStaleQuestion = (thread: OrchestrationV2ThreadShell) => +// Compare against the work start: wakes and restarts may continue a pending +// question, whereas a fresh run must not revive one from an earlier run. +const isStaleQuestion = ( + thread: Pick, +) => thread.pendingRuntimeRequest?.kind === "user_input" && thread.activityRunStartedAt != null && DateTime.toEpochMillis(thread.pendingRuntimeRequest.createdAt) <