authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-24 12:21:27-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-24 12:21:27-07:00
logcdb26fe63549df633751ab0eed145b9357b30a21
tree857f6bbbd1585cf6aaac0359e98432faff8023fd
parent87d84280192195b338e27acf2333771a13d04e61
signature Signed by PGP key B5690EEEBB952194

fix(antigravity): let Stop end commands that outlived their turn (#13388)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

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

apps/server/src/provider/Layers/AntigravityAdapter.test.ts+38
...@@ -775,6 +775,44 @@ it.layer(layer)("AntigravityAdapter", (it) => {...@@ -775,6 +775,44 @@ it.layer(layer)("AntigravityAdapter", (it) => {
775 }),775 }),
776 );776 );
777777
778 it.effect("stops commands left running after a turn when the idle turn is stopped", () =>
779 Effect.gen(function* () {
780 const h = yield* makeHarness();
781 yield* h.adapter.startSession({
782 threadId,
783 cwd: process.cwd(),
784 runtimeMode: "approval-required",
785 });
786 const sending = yield* h.adapter
787 .sendTurn({ threadId, input: "Start a watcher" })
788 .pipe(Effect.forkChild);
789 const prompt = yield* h.nextPrompt;
790 yield* h.emitNative({
791 _tag: "ToolCallUpdated",
792 toolCall: {
793 toolCallId: "watcher-1",
794 kind: "execute",
795 status: "inProgress",
796 command: "tail -f log",
797 data: {},
798 },
799 rawPayload: {},
800 });
801 yield* Deferred.succeed(prompt.result, { stopReason: "end_turn" });
802 yield* Fiber.join(sending);
803 const started = yield* h.waitForEvent((event) => event.type === "task.started");
804
805 // Monitoring's Stop reaches the adapter as a turn interrupt. With no
806 // prompt to cancel, it has to end the session to stop the command.
807 yield* h.adapter.interruptTurn(threadId);
808 const stopped = yield* h.waitForEvent((event) => event.type === "task.completed");
809 expect(stopped.payload).toMatchObject({ taskId: started.payload.taskId, status: "stopped" });
810 yield* h.waitForEvent((event) => event.type === "session.exited");
811 expect(yield* h.adapter.hasSession(threadId)).toBe(false);
812 expect(h.controls.closed).toBe(1);
813 }),
814 );
815
778 it.effect("keeps a launched batch active while child tools continue", () =>816 it.effect("keeps a launched batch active while child tools continue", () =>
779 Effect.gen(function* () {817 Effect.gen(function* () {
780 const h = yield* makeHarness();818 const h = yield* makeHarness();
apps/server/src/provider/Layers/AntigravityAdapter.ts+23-1
...@@ -1165,14 +1165,36 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi...@@ -1165,14 +1165,36 @@ export const makeAntigravityAdapter = Effect.fn("makeAntigravityAdapter")(functi
1165 const interruptTurn: Adapter["interruptTurn"] = (threadId) =>1165 const interruptTurn: Adapter["interruptTurn"] = (threadId) =>
1166 Effect.gen(function* () {1166 Effect.gen(function* () {
1167 const context = yield* requireSession(threadId);1167 const context = yield* requireSession(threadId);
1168 // A command that outlived its turn keeps running in the agent, and
1169 // session/cancel only stops a prompt. The agent kills its background
1170 // commands when its session closes, so Stop with nothing else running
1171 // ends the session, as Claude's does. The next turn resumes it.
1172 let idleWithCommands = false;
1168 yield* context.promptLock1173 yield* context.promptLock
1169 .withPermit(1174 .withPermit(
1170 Effect.gen(function* () {1175 Effect.gen(function* () {
1176 // Decided under the prompt lock so a turn cannot start in between.
1177 if (!context.promptFiber && [...context.commands.values()].some((c) => c.promoted)) {
1178 context.stopped = true;
1179 idleWithCommands = true;
1180 return;
1181 }
1171 yield* cancelRequests(context);1182 yield* cancelRequests(context);
1172 yield* context.runtime.cancel;1183 yield* context.runtime.cancel;
1173 }),1184 }),
1174 )1185 )
1175 .pipe(Effect.mapError((cause) => mapAntigravityError(threadId, "session/cancel", cause)));1186 .pipe(
1187 Effect.mapError((cause) => mapAntigravityError(threadId, "session/cancel", cause)),
1188 // Once marked stopped the session must close, even if this call is
1189 // interrupted, or it is left unreachable with its commands running.
1190 Effect.ensuring(
1191 Effect.suspend(() =>
1192 idleWithCommands
1193 ? withThreadLock(threadId, stopContext(context)).pipe(Effect.ignore)
1194 : Effect.void,
1195 ),
1196 ),
1197 );
1176 });1198 });
11771199
1178 const respondToRequest: Adapter["respondToRequest"] = (threadId, requestId, decision) =>1200 const respondToRequest: Adapter["respondToRequest"] = (threadId, requestId, decision) =>