| author | |
| committer | |
| log | cdb26fe63549df633751ab0eed145b9357b30a21 |
| tree | 857f6bbbd1585cf6aaac0359e98432faff8023fd |
| parent | 87d84280192195b338e27acf2333771a13d04e61 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 775 | }), |
| 776 | 776 | ); |
| 777 | 777 | |
| 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 | 816 | it.effect("keeps a launched batch active while child tools continue", () => |
| 779 | 817 | Effect.gen(function* () { |
| 780 | 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 | 1165 | const interruptTurn: Adapter["interruptTurn"] = (threadId) => |
| 1166 | 1166 | Effect.gen(function* () { |
| 1167 | 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 | 1173 | yield* context.promptLock |
| 1169 | 1174 | .withPermit( |
| 1170 | 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 | 1182 | yield* cancelRequests(context); |
| 1172 | 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 | }); |
| 1177 | 1199 | |
| 1178 | 1200 | const respondToRequest: Adapter["respondToRequest"] = (threadId, requestId, decision) => |