authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-03 20:29:56-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 20:29:56-07:00
log2675e3c70327719a99af4ae6e53e7b74fb8a9be0
tree7618484879ec293cffe39cbc2ace2b086f33e152
parentbf40fa786c521b552eb554bbd4f2c75c4123cd03
signature Signed by PGP key B5690EEEBB952194

fix(antigravity): keep subagent batches active after launch (#9579)


7 files changed, 227 insertions(+), 210 deletions(-)

apps/server/src/provider/Layers/AntigravityAdapter.test.ts+146-183
......@@ -761,81 +761,70 @@ it.layer(layer)("AntigravityAdapter", (it) => {
761761 }),
762762 );
763763
764 it.effect(
765 "shows concurrent native subagent calls and their results without inventing metadata",
766 () =>
767 Effect.gen(function* () {
768 const h = yield* makeHarness();
769 yield* h.adapter.startSession({
770 threadId,
771 cwd: process.cwd(),
772 runtimeMode: "approval-required",
773 });
774 const sending = yield* h.adapter
775 .sendTurn({ threadId, input: "Review with subagents" })
776 .pipe(Effect.forkChild);
777 const prompt = yield* h.nextPrompt;
778 for (const id of ["trajectory:4", "trajectory:5"]) {
779 yield* h.emitNative(
780 nativeToolUpdate({
781 sessionUpdate: "tool_call",
782 toolCallId: id,
783 title: "Running start_subagent",
784 kind: "other",
785 status: "in_progress",
786 rawInput: {},
787 }),
788 );
789 const running = yield* h.waitForEvent((event) => event.type === "task.progress");
790 expect(running.payload).toEqual({
791 taskId: id,
792 taskType: "subagent",
793 toolUseId: id,
794 title: "Antigravity subagent",
795 description: "Antigravity subagent",
796 status: "running",
797 });
798 yield* h.emitNative(
799 nativeToolUpdate({
800 sessionUpdate: "tool_call_update",
801 toolCallId: id,
802 status: "in_progress",
803 }),
804 );
805 }
806 for (const [id, status, result] of [
807 ["trajectory:4", "completed", "No defects found."],
808 ["trajectory:5", "failed", "Subagent exceeded its limit."],
809 ] as const) {
810 yield* h.emitNative(
811 nativeToolUpdate({
812 sessionUpdate: "tool_call_update",
813 toolCallId: id,
814 status,
815 rawOutput: result,
816 }),
817 );
818 const completed = yield* h.waitForEvent((event) => event.type === "task.completed");
819 expect(completed.payload).toEqual({
820 taskId: id,
821 taskType: "subagent",
822 toolUseId: id,
823 title: "Antigravity subagent",
824 status,
825 summary: result,
826 });
827 }
828 yield* Deferred.succeed(prompt.result, { stopReason: "end_turn" });
829 const turn = yield* Fiber.join(sending);
830 yield* h.waitForEvent((event) => event.type === "turn.completed");
831 expect(
832 h.seen
833 .filter((event) => event.type.startsWith("task."))
834 .every((event) => event.turnId === turn.turnId),
835 ).toBe(true);
836 expect(h.seen.filter((event) => event.type.startsWith("item."))).toHaveLength(0);
837 expect(h.seen.filter((event) => event.type === "task.progress")).toHaveLength(2);
838 }),
764 it.effect("keeps a launched batch active while child tools continue", () =>
765 Effect.gen(function* () {
766 const h = yield* makeHarness();
767 yield* h.adapter.startSession({ threadId, cwd: process.cwd(), runtimeMode: "full-access" });
768 const sending = yield* h.adapter
769 .sendTurn({ threadId, input: "Run two readers in one batch" })
770 .pipe(Effect.forkChild);
771 const prompt = yield* h.nextPrompt;
772 // ACP 1.1.1 capture: one launch call covers both children and returns
773 // only its description before either child finishes.
774 const started = nativeToolUpdate({
775 sessionUpdate: "tool_call",
776 toolCallId: `${nativeSessionId}:2`,
777 title: "Running start_subagent",
778 kind: "other",
779 status: "in_progress",
780 rawInput: {},
781 });
782 yield* h.emitNative(started);
783 yield* h.waitForEvent((event) => event.type === "task.progress");
784 yield* h.emitNative(
785 nativeToolUpdate(
786 {
787 sessionUpdate: "tool_call_update",
788 toolCallId: started.toolCall.toolCallId,
789 status: "completed",
790 rawOutput: "Launch subagents",
791 },
792 started.toolCall,
793 ),
794 );
795 const launched = yield* h.waitForEvent((event) => event.type === "task.progress");
796 expect(launched.payload).toMatchObject({
797 taskId: started.toolCall.toolCallId,
798 title: "Antigravity subagent batch",
799 description: "Launch subagents",
800 status: "running",
801 });
802 for (const child of ["alpha", "beta"]) {
803 yield* h.emitNative(
804 nativeToolUpdate({
805 sessionUpdate: "tool_call",
806 toolCallId: `${child}:1`,
807 title: "Read file",
808 kind: "read",
809 status: "completed",
810 rawOutput: "File contents",
811 }),
812 );
813 }
814 yield* h.drainEvents;
815 expect(h.seen.filter((event) => event.type === "task.completed")).toHaveLength(0);
816 expect(h.seen.filter((event) => event.type === "task.updated")).toHaveLength(0);
817 yield* Deferred.succeed(prompt.result, { stopReason: "end_turn" });
818 yield* Fiber.join(sending);
819 yield* h.waitForEvent((event) => event.type === "turn.completed");
820 expect(h.seen.filter((event) => event.type === "task.completed")).toHaveLength(0);
821 expect(h.seen.find((event) => event.type === "task.updated")?.payload).toMatchObject({
822 taskId: started.toolCall.toolCallId,
823 status: "idle",
824 description: "Turn ended. Individual agent status is unavailable.",
825 timelineBypass: true,
826 });
827 }),
839828 );
840829
841830 it.effect("waits for a replayed subagent's final status and result", () =>
......@@ -870,7 +859,7 @@ it.layer(layer)("AntigravityAdapter", (it) => {
870859 taskId: "replayed:4",
871860 taskType: "subagent",
872861 toolUseId: "replayed:4",
873 title: "Antigravity subagent",
862 title: "Antigravity subagent batch",
874863 status: "failed",
875864 summary: "Review failed.",
876865 });
......@@ -878,128 +867,93 @@ it.layer(layer)("AntigravityAdapter", (it) => {
878867 }),
879868 );
880869
881 it.effect("completes a live subagent delivered in one tool call", () =>
870 it.effect("keeps one-message launches active and ignores late updates after settlement", () =>
882871 Effect.gen(function* () {
883872 const h = yield* makeHarness();
884 yield* h.adapter.startSession({
885 threadId,
886 cwd: process.cwd(),
887 runtimeMode: "approval-required",
888 });
889 const sending = yield* h.adapter
890 .sendTurn({ threadId, input: "Review with subagents" })
873 yield* h.adapter.startSession({ threadId, cwd: process.cwd(), runtimeMode: "full-access" });
874 const first = yield* h.adapter
875 .sendTurn({ threadId, input: "Start readers" })
891876 .pipe(Effect.forkChild);
892 const prompt = yield* h.nextPrompt;
893 for (const [id, rawOutput] of [
894 ["live:4", "Review complete."],
895 ["live:5", undefined],
896 ] as const) {
897 yield* h.emitNative(
898 nativeToolUpdate({
899 sessionUpdate: "tool_call",
900 toolCallId: id,
901 title: "Running start_subagent",
902 kind: "other",
903 status: "completed",
904 rawInput: {},
905 ...(rawOutput ? { rawOutput } : {}),
906 }),
907 );
908 const completed = yield* h.waitForEvent((event) => event.type === "task.completed");
909 expect(completed.payload).toMatchObject({ taskId: id, status: "completed" });
910 expect(completed.payload.summary).toBe(rawOutput);
911 }
912 yield* Deferred.succeed(prompt.result, { stopReason: "end_turn" });
913 yield* Fiber.join(sending);
914 yield* h.waitForEvent((event) => event.type === "turn.completed");
915 expect(h.seen.filter((event) => event.type === "task.updated")).toHaveLength(0);
916 }),
917 );
918
919 for (const settlement of ["cancelled", "interrupted", "completed"] as const) {
920 it.effect(`does not reopen a ${settlement} subagent on late merged updates`, () =>
921 Effect.gen(function* () {
922 const h = yield* makeHarness();
923 yield* h.adapter.startSession({
924 threadId,
925 cwd: process.cwd(),
926 runtimeMode: "approval-required",
927 });
928 const first = yield* h.adapter
929 .sendTurn({ threadId, input: "Start review" })
930 .pipe(Effect.forkChild);
931 const firstPrompt = yield* h.nextPrompt;
932 const started = nativeToolUpdate({
877 const firstPrompt = yield* h.nextPrompt;
878 const launches = ["Launch readers", undefined].map((rawOutput, index) =>
879 nativeToolUpdate({
933880 sessionUpdate: "tool_call",
934 toolCallId: "old:4",
881 toolCallId: `old:${index}`,
935882 title: "Running start_subagent",
936883 kind: "other",
937 status: "in_progress",
884 status: "completed",
938885 rawInput: {},
939 });
940 yield* h.emitNative(started);
941 yield* h.waitForEvent((event) => event.type === "task.progress");
942 if (settlement === "cancelled") {
943 yield* h.adapter.interruptTurn(threadId);
944 } else {
945 if (settlement === "completed") {
946 yield* h.emitNative(
947 nativeToolUpdate(
948 {
949 sessionUpdate: "tool_call_update",
950 toolCallId: "old:4",
951 status: "completed",
952 rawOutput: "Original result.",
953 },
954 started.toolCall,
955 ),
956 );
957 }
958 yield* Deferred.succeed(firstPrompt.result, { stopReason: "end_turn" });
959 }
960 yield* Fiber.join(first);
961 yield* h.waitForEvent((event) => event.type === "turn.completed");
962 const second = yield* h.adapter
963 .sendTurn({ threadId, input: "Next review" })
964 .pipe(Effect.forkChild);
965 const secondPrompt = yield* h.nextPrompt;
966 for (const status of ["in_progress", "completed"] as const) {
886 ...(rawOutput ? { rawOutput } : {}),
887 }),
888 );
889 for (const launch of launches) yield* h.emitNative(launch);
890 yield* h.drainEvents;
891 expect(h.seen.filter((event) => event.type === "task.progress")).toHaveLength(2);
892 expect(h.seen.filter((event) => event.type === "task.completed")).toHaveLength(0);
893 yield* Deferred.succeed(firstPrompt.result, { stopReason: "end_turn" });
894 yield* Fiber.join(first);
895 yield* h.waitForEvent((event) => event.type === "turn.completed");
896 const second = yield* h.adapter
897 .sendTurn({ threadId, input: "Next task" })
898 .pipe(Effect.forkChild);
899 const secondPrompt = yield* h.nextPrompt;
900 for (const launch of launches) {
901 for (const status of ["in_progress", "completed", "failed"] as const) {
967902 yield* h.emitNative(
968903 nativeToolUpdate(
969904 {
970905 sessionUpdate: "tool_call_update",
971 toolCallId: "old:4",
906 toolCallId: launch.toolCall.toolCallId,
972907 status,
973 rawOutput: "Late result.",
908 rawOutput: "Late update",
974909 },
975 started.toolCall,
910 launch.toolCall,
976911 ),
977912 );
978913 }
979 yield* h.emitNative(
980 nativeToolUpdate({
981 sessionUpdate: "tool_call",
982 toolCallId: "new:4",
983 title: "Running start_subagent",
984 kind: "other",
914 }
915 yield* h.drainEvents;
916 expect(h.seen.filter((event) => event.type === "task.progress")).toHaveLength(2);
917 expect(h.seen.filter((event) => event.type === "task.updated")).toHaveLength(2);
918 expect(h.seen.filter((event) => event.type === "task.completed")).toHaveLength(0);
919 yield* Deferred.succeed(secondPrompt.result, { stopReason: "end_turn" });
920 yield* Fiber.join(second);
921 }),
922 );
923
924 it.effect("does not report a historical launch as running or completed work", () =>
925 Effect.gen(function* () {
926 const h = yield* makeHarness();
927 yield* h.adapter.startSession({ threadId, cwd: process.cwd(), runtimeMode: "full-access" });
928 const launch = nativeToolUpdate({
929 sessionUpdate: "tool_call",
930 toolCallId: "history:2",
931 title: "Running start_subagent",
932 kind: "other",
933 status: "completed",
934 rawInput: {},
935 });
936 yield* h.emitNative(launch);
937 yield* h.emitNative(
938 nativeToolUpdate(
939 {
940 sessionUpdate: "tool_call_update",
941 toolCallId: launch.toolCall.toolCallId,
985942 status: "completed",
986 rawOutput: "New result.",
987 }),
988 );
989 const completed = yield* h.waitForEvent((event) => event.type === "task.completed");
990 expect(completed.payload.taskId).toBe("new:4");
991 yield* Deferred.succeed(secondPrompt.result, { stopReason: "end_turn" });
992 yield* Fiber.join(second);
993 yield* h.waitForEvent((event) => event.type === "turn.completed");
994 expect(h.seen.filter((event) => event.type === "task.progress")).toHaveLength(1);
995 expect(
996 h.seen.filter(
997 (event) => event.type === "task.completed" && event.payload.taskId === "old:4",
998 ),
999 ).toHaveLength(settlement === "completed" ? 1 : 0);
1000 }),
1001 );
1002 }
943 rawOutput: "Launch readers",
944 },
945 launch.toolCall,
946 ),
947 );
948 yield* h.drainEvents;
949 expect(h.seen.filter((event) => event.type.startsWith("task."))).toMatchObject([
950 {
951 type: "task.updated",
952 payload: { status: "idle", timelineBypass: true },
953 },
954 ]);
955 }),
956 );
1003957
1004958 it.effect("keeps MCP identity when later updates omit metadata", () =>
1005959 Effect.gen(function* () {
......@@ -1101,6 +1055,15 @@ it.layer(layer)("AntigravityAdapter", (it) => {
11011055 }),
11021056 );
11031057 yield* h.waitForEvent((event) => event.type === "task.progress");
1058 yield* h.emitNative(
1059 nativeToolUpdate({
1060 sessionUpdate: "tool_call_update",
1061 toolCallId: "trajectory:4",
1062 status: "completed",
1063 rawOutput: "Launch subagents",
1064 }),
1065 );
1066 yield* h.waitForEvent((event) => event.type === "task.progress");
11041067 if (stop === "disconnect") {
11051068 yield* h.emitNative({
11061069 _tag: "ConnectionTerminated",
......@@ -1121,14 +1084,14 @@ it.layer(layer)("AntigravityAdapter", (it) => {
11211084 const settled = yield* h.waitForEvent((event) => event.type === "task.updated");
11221085 expect(settled.payload).toMatchObject({
11231086 taskId: "trajectory:4",
1124 title: "Antigravity subagent",
1087 title: "Antigravity subagent batch",
11251088 taskType: "subagent",
11261089 status:
11271090 stop === "disconnect"
11281091 ? "failed"
11291092 : stop === "cancel" || stop === "steer"
11301093 ? "cancelled"
1131 : "interrupted",
1094 : "idle",
11321095 });
11331096 if (stop === "disconnect")
11341097 yield* h.waitForEvent((event) => event.type === "session.exited");
apps/server/src/provider/Layers/AntigravityAdapter.ts+36-13
......@@ -71,7 +71,7 @@ import {
7171} from "../acp/AntigravityAcpSupport.ts";
7272import {
7373 antigravityApprovalOptions,
74 antigravitySubagentResult,
74 antigravitySubagentOutput,
7575 classifyAntigravitySubagentToolCall,
7676 extractAntigravityUserInputQuestion,
7777 isAntigravityOpenCommand,
......@@ -170,6 +170,7 @@ interface OpenCommand {
170170interface OpenSubagent {
171171 readonly turnId: TurnId | undefined;
172172 readonly status: "pending" | "running" | undefined;
173 readonly description?: string;
173174}
174175
175176function subagentLinkage(toolCallId: string) {
......@@ -177,7 +178,7 @@ function subagentLinkage(toolCallId: string) {
177178 taskId: RuntimeTaskId.make(toolCallId),
178179 taskType: "subagent",
179180 toolUseId: toolCallId,
180 title: "Antigravity subagent",
181 title: "Antigravity subagent batch",
181182 };
182183}
183184
......@@ -389,7 +390,7 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
389390
390391 const finishSubagents = (
391392 context: SessionContext,
392 status: Extract<RuntimeTaskStatus, "cancelled" | "failed" | "interrupted">,
393 status: Extract<RuntimeTaskStatus, "cancelled" | "failed" | "idle">,
393394 error?: string,
394395 ) =>
395396 context.commandLock.withPermit(
......@@ -405,6 +406,12 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
405406 payload: {
406407 ...subagentLinkage(id),
407408 status,
409 ...(status === "idle"
410 ? {
411 description: "Turn ended. Individual agent status is unavailable.",
412 timelineBypass: true,
413 }
414 : {}),
408415 ...(error ? { error } : {}),
409416 },
410417 });
......@@ -628,8 +635,8 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
628635 context.subagents.set(toolCall.toolCallId, { turnId, status: undefined });
629636 return;
630637 }
631 if (toolCall.status === "completed" || toolCall.status === "failed") {
632 const summary = antigravitySubagentResult(toolCall);
638 if (toolCall.status === "failed") {
639 const summary = antigravitySubagentOutput(toolCall);
633640 yield* emit({
634641 type: "task.completed",
635642 ...(yield* stamp),
......@@ -643,19 +650,38 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
643650 },
644651 });
645652 context.subagents.set(toolCall.toolCallId, "finished");
653 } else if (context.activeTurnId === undefined && toolCall.status === "completed") {
654 yield* emit({
655 type: "task.updated",
656 ...(yield* stamp),
657 provider: PROVIDER,
658 threadId: context.threadId,
659 turnId,
660 payload: {
661 ...linkage,
662 status: "idle",
663 description: "Individual agent status is unavailable for this earlier batch.",
664 timelineBypass: true,
665 },
666 });
667 context.subagents.set(toolCall.toolCallId, "finished");
646668 } else {
669 // start_subagent returns after launching a batch. Its output is
670 // the launch description, not a child result or completion.
647671 const status = toolCall.status === "pending" ? "pending" : "running";
648 if (subagent?.status !== status) {
672 const description =
673 antigravitySubagentOutput(toolCall) ?? subagent?.description ?? linkage.title;
674 if (subagent?.status !== status || subagent?.description !== description) {
649675 yield* emit({
650676 type: "task.progress",
651677 ...(yield* stamp),
652678 provider: PROVIDER,
653679 threadId: context.threadId,
654680 turnId,
655 payload: { ...linkage, description: linkage.title, status },
681 payload: { ...linkage, description, summary: description, status },
656682 });
657683 }
658 context.subagents.set(toolCall.toolCallId, { turnId, status });
684 context.subagents.set(toolCall.toolCallId, { turnId, status, description });
659685 }
660686 return;
661687 }
......@@ -972,11 +998,8 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
972998 ? "cancelled"
973999 : payload.state === "failed"
9741000 ? "failed"
975 : "interrupted",
976 payload.errorMessage ??
977 (payload.state === "completed"
978 ? "Antigravity ended the turn before reporting a subagent result."
979 : undefined),
1001 : "idle",
1002 payload.errorMessage,
9801003 );
9811004 context.activeTurnId = undefined;
9821005 context.promptFiber = undefined;
apps/server/src/provider/acp/AntigravityProtocol.test.ts+5-5
......@@ -6,7 +6,7 @@ import {
66 extractAntigravityUserInputQuestion,
77 isAntigravityOpenCommand,
88 antigravityApprovalOptions,
9 antigravitySubagentResult,
9 antigravitySubagentOutput,
1010 isAntigravitySubagentReplayStart,
1111 classifyAntigravitySubagentToolCall,
1212 isAntigravityUserInputRequest,
......@@ -48,7 +48,7 @@ describe("native Antigravity subagent tools", () => {
4848 ).toBeUndefined();
4949 });
5050
51 it("recognizes history starts and bounds the native result", () => {
51 it("recognizes history starts and bounds the launch output", () => {
5252 expect(
5353 isAntigravitySubagentReplayStart({
5454 update: { sessionUpdate: "tool_call", status: "completed", rawOutput: "Done." },
......@@ -65,19 +65,19 @@ describe("native Antigravity subagent tools", () => {
6565 }),
6666 ).toBe(false);
6767 expect(
68 antigravitySubagentResult({
68 antigravitySubagentOutput({
6969 toolCallId: "trajectory:4",
7070 data: { rawOutput: " Finished review. " },
7171 }),
7272 ).toBe("Finished review.");
73 const result = antigravitySubagentResult({
73 const result = antigravitySubagentOutput({
7474 toolCallId: "trajectory:4",
7575 data: { rawOutput: `${"x".repeat(16_000)}The result.` },
7676 });
7777 expect(result?.length).toBeLessThan(8_100);
7878 expect(result?.endsWith("The result.")).toBe(true);
7979 expect(
80 antigravitySubagentResult({ toolCallId: "trajectory:4", data: { rawOutput: {} } }),
80 antigravitySubagentOutput({ toolCallId: "trajectory:4", data: { rawOutput: {} } }),
8181 ).toBeUndefined();
8282 });
8383});
apps/server/src/provider/acp/AntigravityProtocol.ts+1-1
......@@ -376,7 +376,7 @@ export function isAntigravitySubagentReplayStart(rawPayload: unknown): boolean {
376376 );
377377}
378378
379export function antigravitySubagentResult(toolCall: AcpToolCallState): string | undefined {
379export function antigravitySubagentOutput(toolCall: AcpToolCallState): string | undefined {
380380 const output = toolCall.data.rawOutput;
381381 return typeof output === "string" && output.trim() ? boundText(output.trim()) : undefined;
382382}
docs/user/providers-antigravity.md+9-8
......@@ -141,14 +141,15 @@ follow-up message or start a new thread instead.
141141
142142### Subagents
143143
144Antigravity subagent calls appear in **Agents** on web and desktop, and in the work log on
145mobile. Each call shows its status and the result or error returned by Antigravity. Calls
146that run at the same time have separate entries.
147
148The official ACP agent does not report subagent names, models, token usage, or parent links.
149Entries use the name **Antigravity subagent**. Child tool calls cannot be assigned to an entry
150because ACP does not include their owning subagent. These entries track each invocation,
151not a separate thread you can open or control.
144Subagent launches appear as **Antigravity subagent batch** in **Agents** on web and desktop,
145and in the work log on mobile. One launch can start several agents. The batch stays active
146after launch while the parent turn runs. When that turn ends, the entry becomes idle and
147states that individual agent status is unavailable. Launch errors remain visible.
148
149The official ACP agent does not send individual child status, names, models, token usage,
150or reply ownership. T3 Code cannot show separate child entries or separate child replies
151from the parent conversation. The launch description is not a child result. Batch entries
152cannot be opened or controlled as separate threads.
152153
153154## Accounts and removal
154155
packages/client-runtime/src/state/subagentRuntime.test.ts+28
......@@ -66,6 +66,34 @@ function fold(rows: ReadonlyArray<OrchestrationThreadActivity>) {
6666}
6767
6868describe("foldSubagentActivities", () => {
69 it("shows the batch status limit after its parent turn ends without claiming a result", () => {
70 const running = activity("task.progress", {
71 taskId: "batch-1",
72 taskType: "subagent",
73 title: "Antigravity subagent batch",
74 status: "running",
75 summary: "Launch readers",
76 });
77 const agents = fold([
78 running,
79 activity("task.updated", {
80 taskId: "batch-1",
81 taskType: "subagent",
82 status: "idle",
83 detail: "Turn ended. Individual agent status is unavailable.",
84 timelineBypass: true,
85 }),
86 ]);
87 expect(agents).toHaveLength(1);
88 expect(agents[0]).toMatchObject({
89 title: "Antigravity subagent batch",
90 status: "idle",
91 progress: "Turn ended. Individual agent status is unavailable.",
92 result: null,
93 error: null,
94 });
95 });
96
6997 it("builds an agent from start → progress → completion", () => {
7098 const agents = fold([
7199 activity("task.started", {
packages/client-runtime/src/state/subagentRuntime.ts+2
......@@ -546,6 +546,8 @@ export function foldSubagentActivities(
546546 if (!agents.has(taskId) && isBackgroundTaskActivity(payload)) break;
547547 const agent = getOrCreate(agents, taskId, payload, at);
548548 fillMetadata(agent, payload);
549 const detail = asString(payload.detail);
550 if (detail) agent.progress = bounded(detail);
549551 // A task first seen via task.updated (start row aged out) has run at
550552 // least once — zero activations would misreport "run 0" and let a
551553 // later start row treat it as never-started (review finding).