authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-03 14:28:20-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-03 14:28:20-07:00
loga21702d0e58ad20263dc965d84472bb6d7eb38c8
treeec4194c169a9e3e0a8756f1293387402a67193f1
parentf1dcd93931b322d587a7253856f44625a6afbce0
signature Signed by PGP key B5690EEEBB952194

test(server): ACP adapter tests no longer race the prompt settle (#15330)

Takes over #14876. Co-authored-by: tris203 <admin@snappeh.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

1 files changed, 80 insertions(+), 198 deletions(-)

apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts+80-198
...@@ -4925,9 +4925,13 @@ describe("AcpAdapterV2", () => {...@@ -4925,9 +4925,13 @@ describe("AcpAdapterV2", () => {
4925 const mockAgentPath = yield* path.fromFileUrl(4925 const mockAgentPath = yield* path.fromFileUrl(
4926 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),4926 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
4927 );4927 );
4928 const protocolEvents = yield* Queue.bounded<EffectAcpProtocol.AcpProtocolLogEvent>(256);
4929 const instanceId = ProviderInstanceId.make("acp-test");4928 const instanceId = ProviderInstanceId.make("acp-test");
4929 const promptSettled = yield* Deferred.make<void>();
4930 const adapter = makeAcpAdapterV2({4930 const adapter = makeAcpAdapterV2({
4931 testHooks: {
4932 afterPromptSettledWithBackgroundWork: () =>
4933 Deferred.succeed(promptSettled, undefined).pipe(Effect.asVoid),
4934 },
4931 crypto: yield* Crypto.Crypto,4935 crypto: yield* Crypto.Crypto,
4932 instanceId,4936 instanceId,
4933 flavor: {4937 flavor: {
...@@ -4950,7 +4954,6 @@ describe("AcpAdapterV2", () => {...@@ -4950,7 +4954,6 @@ describe("AcpAdapterV2", () => {
4950 childProcessSpawner,4954 childProcessSpawner,
4951 mockAgentPath,4955 mockAgentPath,
4952 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },4956 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },
4953 protocolEvents,
4954 }),4957 }),
4955 },4958 },
4956 fileSystem,4959 fileSystem,
...@@ -4986,28 +4989,13 @@ describe("AcpAdapterV2", () => {...@@ -4986,28 +4989,13 @@ describe("AcpAdapterV2", () => {
4986 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),4989 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),
4987 );4990 );
4988 // The still-running subagent defers finalize after session/prompt returns.4991 // The still-running subagent defers finalize after session/prompt returns.
4989 yield* Stream.fromQueue(protocolEvents).pipe(4992 yield* Deferred.await(promptSettled);
4990 Stream.filter(
4991 (event) =>
4992 event.direction === "incoming" &&
4993 event.stage === "raw" &&
4994 typeof event.payload === "string" &&
4995 event.payload.includes('"stopReason"'),
4996 ),
4997 Stream.runHead,
4998 );
4999 yield* Effect.yieldNow;
5000 yield* Effect.yieldNow;
50014993
5002 const providerTurnId = idAllocator.derive.providerTurn({4994 const providerTurnId = idAllocator.derive.providerTurn({
5003 driver: ACP_TEST_DRIVER,4995 driver: ACP_TEST_DRIVER,
5004 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),4996 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),
5005 });4997 });
5006 const interruptFiber = yield* runtime4998 yield* runtime.interruptTurn({ providerThread, providerTurnId });
5007 .interruptTurn({ providerThread, providerTurnId })
5008 .pipe(Effect.forkScoped);
5009 yield* TestClock.adjust("10 seconds");
5010 yield* Fiber.join(interruptFiber);
50114999
5012 let terminalStatus: string | null = null;5000 let terminalStatus: string | null = null;
5013 while (terminalStatus === null) {5001 while (terminalStatus === null) {
...@@ -5033,10 +5021,14 @@ describe("AcpAdapterV2", () => {...@@ -5033,10 +5021,14 @@ describe("AcpAdapterV2", () => {
5033 const mockAgentPath = yield* path.fromFileUrl(5021 const mockAgentPath = yield* path.fromFileUrl(
5034 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),5022 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
5035 );5023 );
5036 const protocolEvents = yield* Queue.bounded<EffectAcpProtocol.AcpProtocolLogEvent>(256);
5037 const instanceId = ProviderInstanceId.make("acp-test");5024 const instanceId = ProviderInstanceId.make("acp-test");
5038 let subagentPhase: "spawn" | "complete" = "spawn";5025 let subagentPhase: "spawn" | "complete" = "spawn";
5026 const promptSettled = yield* Deferred.make<void>();
5039 const adapter = makeAcpAdapterV2({5027 const adapter = makeAcpAdapterV2({
5028 testHooks: {
5029 afterPromptSettledWithBackgroundWork: () =>
5030 Deferred.succeed(promptSettled, undefined).pipe(Effect.asVoid),
5031 },
5040 crypto: yield* Crypto.Crypto,5032 crypto: yield* Crypto.Crypto,
5041 instanceId,5033 instanceId,
5042 flavor: {5034 flavor: {
...@@ -5072,7 +5064,6 @@ describe("AcpAdapterV2", () => {...@@ -5072,7 +5064,6 @@ describe("AcpAdapterV2", () => {
5072 childProcessSpawner,5064 childProcessSpawner,
5073 mockAgentPath,5065 mockAgentPath,
5074 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },5066 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },
5075 protocolEvents,
5076 }),5067 }),
5077 },5068 },
5078 fileSystem,5069 fileSystem,
...@@ -5107,28 +5098,13 @@ describe("AcpAdapterV2", () => {...@@ -5107,28 +5098,13 @@ describe("AcpAdapterV2", () => {
5107 yield* runtime.startTurn(5098 yield* runtime.startTurn(
5108 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),5099 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),
5109 );5100 );
5110 yield* Stream.fromQueue(protocolEvents).pipe(5101 yield* Deferred.await(promptSettled);
5111 Stream.filter(
5112 (event) =>
5113 event.direction === "incoming" &&
5114 event.stage === "raw" &&
5115 typeof event.payload === "string" &&
5116 event.payload.includes('"stopReason"'),
5117 ),
5118 Stream.runHead,
5119 );
5120 yield* Effect.yieldNow;
5121 yield* Effect.yieldNow;
51225102
5123 const firstProviderTurnId = idAllocator.derive.providerTurn({5103 const firstProviderTurnId = idAllocator.derive.providerTurn({
5124 driver: ACP_TEST_DRIVER,5104 driver: ACP_TEST_DRIVER,
5125 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),5105 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),
5126 });5106 });
5127 const interruptFiber = yield* runtime5107 yield* runtime.interruptTurn({ providerThread, providerTurnId: firstProviderTurnId });
5128 .interruptTurn({ providerThread, providerTurnId: firstProviderTurnId })
5129 .pipe(Effect.forkScoped);
5130 yield* TestClock.adjust("10 seconds");
5131 yield* Fiber.join(interruptFiber);
51325108
5133 let subagentTurnItemId: string | null = null;5109 let subagentTurnItemId: string | null = null;
5134 let firstTerminalStatus: string | null = null;5110 let firstTerminalStatus: string | null = null;
...@@ -5193,10 +5169,14 @@ describe("AcpAdapterV2", () => {...@@ -5193,10 +5169,14 @@ describe("AcpAdapterV2", () => {
5193 const mockAgentPath = yield* path.fromFileUrl(5169 const mockAgentPath = yield* path.fromFileUrl(
5194 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),5170 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
5195 );5171 );
5196 const protocolEvents = yield* Queue.bounded<EffectAcpProtocol.AcpProtocolLogEvent>(256);
5197 const instanceId = ProviderInstanceId.make("acp-test");5172 const instanceId = ProviderInstanceId.make("acp-test");
5198 let subagentPhase: "spawn" | "complete" = "spawn";5173 let subagentPhase: "spawn" | "complete" = "spawn";
5174 const promptSettled = yield* Deferred.make<void>();
5199 const adapter = makeAcpAdapterV2({5175 const adapter = makeAcpAdapterV2({
5176 testHooks: {
5177 afterPromptSettledWithBackgroundWork: () =>
5178 Deferred.succeed(promptSettled, undefined).pipe(Effect.asVoid),
5179 },
5200 crypto: yield* Crypto.Crypto,5180 crypto: yield* Crypto.Crypto,
5201 instanceId,5181 instanceId,
5202 flavor: {5182 flavor: {
...@@ -5230,7 +5210,6 @@ describe("AcpAdapterV2", () => {...@@ -5230,7 +5210,6 @@ describe("AcpAdapterV2", () => {
5230 childProcessSpawner,5210 childProcessSpawner,
5231 mockAgentPath,5211 mockAgentPath,
5232 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },5212 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },
5233 protocolEvents,
5234 }),5213 }),
5235 },5214 },
5236 fileSystem,5215 fileSystem,
...@@ -5272,28 +5251,13 @@ describe("AcpAdapterV2", () => {...@@ -5272,28 +5251,13 @@ describe("AcpAdapterV2", () => {
5272 yield* runtime.startTurn(5251 yield* runtime.startTurn(
5273 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),5252 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),
5274 );5253 );
5275 yield* Stream.fromQueue(protocolEvents).pipe(5254 yield* Deferred.await(promptSettled);
5276 Stream.filter(
5277 (event) =>
5278 event.direction === "incoming" &&
5279 event.stage === "raw" &&
5280 typeof event.payload === "string" &&
5281 event.payload.includes('"stopReason"'),
5282 ),
5283 Stream.runHead,
5284 );
5285 yield* Effect.yieldNow;
5286 yield* Effect.yieldNow;
52875255
5288 const firstProviderTurnId = idAllocator.derive.providerTurn({5256 const firstProviderTurnId = idAllocator.derive.providerTurn({
5289 driver: ACP_TEST_DRIVER,5257 driver: ACP_TEST_DRIVER,
5290 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),5258 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),
5291 });5259 });
5292 const interruptFiber = yield* runtime5260 yield* runtime.interruptTurn({ providerThread, providerTurnId: firstProviderTurnId });
5293 .interruptTurn({ providerThread, providerTurnId: firstProviderTurnId })
5294 .pipe(Effect.forkScoped);
5295 yield* TestClock.adjust("10 seconds");
5296 yield* Fiber.join(interruptFiber);
52975261
5298 let firstTerminalStatus: string | null = null;5262 let firstTerminalStatus: string | null = null;
5299 while (firstTerminalStatus === null) {5263 while (firstTerminalStatus === null) {
...@@ -5352,7 +5316,6 @@ describe("AcpAdapterV2", () => {...@@ -5352,7 +5316,6 @@ describe("AcpAdapterV2", () => {
5352 const mockAgentPath = yield* path.fromFileUrl(5316 const mockAgentPath = yield* path.fromFileUrl(
5353 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),5317 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
5354 );5318 );
5355 const protocolEvents = yield* Queue.bounded<EffectAcpProtocol.AcpProtocolLogEvent>(256);
5356 const secondPromptWireReturned = yield* Deferred.make<void>();5319 const secondPromptWireReturned = yield* Deferred.make<void>();
5357 const releaseSecondPromptCompletion = yield* Deferred.make<void>();5320 const releaseSecondPromptCompletion = yield* Deferred.make<void>();
5358 const instanceId = ProviderInstanceId.make("acp-test");5321 const instanceId = ProviderInstanceId.make("acp-test");
...@@ -5361,7 +5324,12 @@ describe("AcpAdapterV2", () => {...@@ -5361,7 +5324,12 @@ describe("AcpAdapterV2", () => {
5361 let subagentPhase: "spawn" | "complete" = "spawn";5324 let subagentPhase: "spawn" | "complete" = "spawn";
5362 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];5325 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];
5363 let sessionUpdateHandler: Parameters<RuntimeService["handleSessionUpdate"]>[0] | undefined;5326 let sessionUpdateHandler: Parameters<RuntimeService["handleSessionUpdate"]>[0] | undefined;
5327 const promptSettled = yield* Deferred.make<void>();
5364 const adapter = makeAcpAdapterV2({5328 const adapter = makeAcpAdapterV2({
5329 testHooks: {
5330 afterPromptSettledWithBackgroundWork: () =>
5331 Deferred.succeed(promptSettled, undefined).pipe(Effect.asVoid),
5332 },
5365 crypto: yield* Crypto.Crypto,5333 crypto: yield* Crypto.Crypto,
5366 instanceId,5334 instanceId,
5367 flavor: {5335 flavor: {
...@@ -5395,7 +5363,6 @@ describe("AcpAdapterV2", () => {...@@ -5395,7 +5363,6 @@ describe("AcpAdapterV2", () => {
5395 childProcessSpawner,5363 childProcessSpawner,
5396 mockAgentPath,5364 mockAgentPath,
5397 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },5365 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },
5398 protocolEvents,
5399 wrapRuntime: (runtime) => ({5366 wrapRuntime: (runtime) => ({
5400 ...runtime,5367 ...runtime,
5401 handleSessionUpdate: (handler) =>5368 handleSessionUpdate: (handler) =>
...@@ -5456,28 +5423,13 @@ describe("AcpAdapterV2", () => {...@@ -5456,28 +5423,13 @@ describe("AcpAdapterV2", () => {
5456 yield* runtime.startTurn(5423 yield* runtime.startTurn(
5457 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),5424 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),
5458 );5425 );
5459 yield* Stream.fromQueue(protocolEvents).pipe(5426 yield* Deferred.await(promptSettled);
5460 Stream.filter(
5461 (event) =>
5462 event.direction === "incoming" &&
5463 event.stage === "raw" &&
5464 typeof event.payload === "string" &&
5465 event.payload.includes('"stopReason"'),
5466 ),
5467 Stream.runHead,
5468 );
5469 yield* Effect.yieldNow;
5470 yield* Effect.yieldNow;
54715427
5472 const firstProviderTurnId = idAllocator.derive.providerTurn({5428 const firstProviderTurnId = idAllocator.derive.providerTurn({
5473 driver: ACP_TEST_DRIVER,5429 driver: ACP_TEST_DRIVER,
5474 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),5430 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),
5475 });5431 });
5476 const interruptFiber = yield* runtime5432 yield* runtime.interruptTurn({ providerThread, providerTurnId: firstProviderTurnId });
5477 .interruptTurn({ providerThread, providerTurnId: firstProviderTurnId })
5478 .pipe(Effect.forkScoped);
5479 yield* TestClock.adjust("10 seconds");
5480 yield* Fiber.join(interruptFiber);
54815433
5482 let firstTerminalStatus: string | null = null;5434 let firstTerminalStatus: string | null = null;
5483 while (firstTerminalStatus === null) {5435 while (firstTerminalStatus === null) {
...@@ -5601,7 +5553,6 @@ describe("AcpAdapterV2", () => {...@@ -5601,7 +5553,6 @@ describe("AcpAdapterV2", () => {
5601 const mockAgentPath = yield* path.fromFileUrl(5553 const mockAgentPath = yield* path.fromFileUrl(
5602 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),5554 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
5603 );5555 );
5604 const protocolEvents = yield* Queue.bounded<EffectAcpProtocol.AcpProtocolLogEvent>(256);
5605 const baseClock = yield* Clock.Clock;5556 const baseClock = yield* Clock.Clock;
5606 const finalizationClockRead = yield* Deferred.make<void>();5557 const finalizationClockRead = yield* Deferred.make<void>();
5607 const releaseFinalizationClockRead = yield* Deferred.make<void>();5558 const releaseFinalizationClockRead = yield* Deferred.make<void>();
...@@ -5629,7 +5580,12 @@ describe("AcpAdapterV2", () => {...@@ -5629,7 +5580,12 @@ describe("AcpAdapterV2", () => {
5629 let sessionUpdateHandler:5580 let sessionUpdateHandler:
5630 | Parameters<RuntimeService["handleSessionUpdate"]>[0]5581 | Parameters<RuntimeService["handleSessionUpdate"]>[0]
5631 | undefined;5582 | undefined;
5583 const promptSettled = yield* Deferred.make<void>();
5632 const adapter = makeAcpAdapterV2({5584 const adapter = makeAcpAdapterV2({
5585 testHooks: {
5586 afterPromptSettledWithBackgroundWork: () =>
5587 Deferred.succeed(promptSettled, undefined).pipe(Effect.asVoid),
5588 },
5633 crypto: yield* Crypto.Crypto,5589 crypto: yield* Crypto.Crypto,
5634 instanceId,5590 instanceId,
5635 flavor: {5591 flavor: {
...@@ -5668,7 +5624,6 @@ describe("AcpAdapterV2", () => {...@@ -5668,7 +5624,6 @@ describe("AcpAdapterV2", () => {
5668 childProcessSpawner,5624 childProcessSpawner,
5669 mockAgentPath,5625 mockAgentPath,
5670 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },5626 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },
5671 protocolEvents,
5672 wrapRuntime: (runtime) => ({5627 wrapRuntime: (runtime) => ({
5673 ...runtime,5628 ...runtime,
5674 handleSessionUpdate: (handler) =>5629 handleSessionUpdate: (handler) =>
...@@ -5719,18 +5674,7 @@ describe("AcpAdapterV2", () => {...@@ -5719,18 +5674,7 @@ describe("AcpAdapterV2", () => {
5719 yield* runtime.startTurn(5674 yield* runtime.startTurn(
5720 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),5675 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),
5721 );5676 );
5722 yield* Stream.fromQueue(protocolEvents).pipe(5677 yield* Deferred.await(promptSettled);
5723 Stream.filter(
5724 (event) =>
5725 event.direction === "incoming" &&
5726 event.stage === "raw" &&
5727 typeof event.payload === "string" &&
5728 event.payload.includes('"stopReason"'),
5729 ),
5730 Stream.runHead,
5731 );
5732 yield* Effect.yieldNow;
5733 yield* Effect.yieldNow;
5734 yield* TestClock.adjust("1 second");5678 yield* TestClock.adjust("1 second");
5735 assert.isDefined(sessionUpdateHandler, "session update handler must be wired");5679 assert.isDefined(sessionUpdateHandler, "session update handler must be wired");
57365680
...@@ -6451,14 +6395,18 @@ describe("AcpAdapterV2", () => {...@@ -6451,14 +6395,18 @@ describe("AcpAdapterV2", () => {
6451 const mockAgentPath = yield* path.fromFileUrl(6395 const mockAgentPath = yield* path.fromFileUrl(
6452 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),6396 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
6453 );6397 );
6454 const protocolEvents = yield* Queue.bounded<EffectAcpProtocol.AcpProtocolLogEvent>(256);
6455 const continuationRequests: Array<ProviderContinuationRequest> = [];6398 const continuationRequests: Array<ProviderContinuationRequest> = [];
6456 const bufferedAssistantText = "BUFFERED_WAKE_AFTER_USER_ATTACH";6399 const bufferedAssistantText = "BUFFERED_WAKE_AFTER_USER_ATTACH";
6457 const instanceId = ProviderInstanceId.make("acp-test");6400 const instanceId = ProviderInstanceId.make("acp-test");
6458 let subagentPhase: "spawn" | "complete" = "spawn";6401 let subagentPhase: "spawn" | "complete" = "spawn";
6459 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];6402 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];
6460 let sessionUpdateHandler: Parameters<RuntimeService["handleSessionUpdate"]>[0] | undefined;6403 let sessionUpdateHandler: Parameters<RuntimeService["handleSessionUpdate"]>[0] | undefined;
6404 const promptSettled = yield* Deferred.make<void>();
6461 const adapter = makeAcpAdapterV2({6405 const adapter = makeAcpAdapterV2({
6406 testHooks: {
6407 afterPromptSettledWithBackgroundWork: () =>
6408 Deferred.succeed(promptSettled, undefined).pipe(Effect.asVoid),
6409 },
6462 crypto: yield* Crypto.Crypto,6410 crypto: yield* Crypto.Crypto,
6463 instanceId,6411 instanceId,
6464 flavor: {6412 flavor: {
...@@ -6492,7 +6440,6 @@ describe("AcpAdapterV2", () => {...@@ -6492,7 +6440,6 @@ describe("AcpAdapterV2", () => {
6492 childProcessSpawner,6440 childProcessSpawner,
6493 mockAgentPath,6441 mockAgentPath,
6494 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },6442 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },
6495 protocolEvents,
6496 wrapRuntime: (runtime) => ({6443 wrapRuntime: (runtime) => ({
6497 ...runtime,6444 ...runtime,
6498 handleSessionUpdate: (handler) =>6445 handleSessionUpdate: (handler) =>
...@@ -6548,28 +6495,13 @@ describe("AcpAdapterV2", () => {...@@ -6548,28 +6495,13 @@ describe("AcpAdapterV2", () => {
6548 yield* runtime.startTurn(6495 yield* runtime.startTurn(
6549 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),6496 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),
6550 );6497 );
6551 yield* Stream.fromQueue(protocolEvents).pipe(6498 yield* Deferred.await(promptSettled);
6552 Stream.filter(
6553 (event) =>
6554 event.direction === "incoming" &&
6555 event.stage === "raw" &&
6556 typeof event.payload === "string" &&
6557 event.payload.includes('"stopReason"'),
6558 ),
6559 Stream.runHead,
6560 );
6561 yield* Effect.yieldNow;
6562 yield* Effect.yieldNow;
65636499
6564 const firstProviderTurnId = idAllocator.derive.providerTurn({6500 const firstProviderTurnId = idAllocator.derive.providerTurn({
6565 driver: ACP_TEST_DRIVER,6501 driver: ACP_TEST_DRIVER,
6566 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),6502 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),
6567 });6503 });
6568 const interruptFiber = yield* runtime6504 yield* runtime.interruptTurn({ providerThread, providerTurnId: firstProviderTurnId });
6569 .interruptTurn({ providerThread, providerTurnId: firstProviderTurnId })
6570 .pipe(Effect.forkScoped);
6571 yield* TestClock.adjust("10 seconds");
6572 yield* Fiber.join(interruptFiber);
65736505
6574 let firstTerminalStatus: string | null = null;6506 let firstTerminalStatus: string | null = null;
6575 while (firstTerminalStatus === null) {6507 while (firstTerminalStatus === null) {
...@@ -6752,14 +6684,18 @@ describe("AcpAdapterV2", () => {...@@ -6752,14 +6684,18 @@ describe("AcpAdapterV2", () => {
6752 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),6684 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
6753 );6685 );
6754 const continuationRequests: Array<ProviderContinuationRequest> = [];6686 const continuationRequests: Array<ProviderContinuationRequest> = [];
6755 const protocolEvents = yield* Queue.bounded<EffectAcpProtocol.AcpProtocolLogEvent>(256);
6756 const promptWireReturned = yield* Deferred.make<void>();6687 const promptWireReturned = yield* Deferred.make<void>();
6757 const releasePromptCompletion = yield* Deferred.make<void>();6688 const releasePromptCompletion = yield* Deferred.make<void>();
6758 const instanceId = ProviderInstanceId.make("acp-test");6689 const instanceId = ProviderInstanceId.make("acp-test");
6759 let subagentPhase: "spawn" | "complete" = "spawn";6690 let subagentPhase: "spawn" | "complete" = "spawn";
6760 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];6691 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];
6761 let sessionUpdateHandler: Parameters<RuntimeService["handleSessionUpdate"]>[0] | undefined;6692 let sessionUpdateHandler: Parameters<RuntimeService["handleSessionUpdate"]>[0] | undefined;
6693 const promptSettled = yield* Deferred.make<void>();
6762 const adapter = makeAcpAdapterV2({6694 const adapter = makeAcpAdapterV2({
6695 testHooks: {
6696 afterPromptSettledWithBackgroundWork: () =>
6697 Deferred.succeed(promptSettled, undefined).pipe(Effect.asVoid),
6698 },
6763 crypto: yield* Crypto.Crypto,6699 crypto: yield* Crypto.Crypto,
6764 instanceId,6700 instanceId,
6765 flavor: {6701 flavor: {
...@@ -6795,7 +6731,6 @@ describe("AcpAdapterV2", () => {...@@ -6795,7 +6731,6 @@ describe("AcpAdapterV2", () => {
6795 childProcessSpawner,6731 childProcessSpawner,
6796 mockAgentPath,6732 mockAgentPath,
6797 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },6733 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },
6798 protocolEvents,
6799 wrapRuntime: (runtime) => ({6734 wrapRuntime: (runtime) => ({
6800 ...runtime,6735 ...runtime,
6801 handleSessionUpdate: (handler) =>6736 handleSessionUpdate: (handler) =>
...@@ -6875,18 +6810,13 @@ describe("AcpAdapterV2", () => {...@@ -6875,18 +6810,13 @@ describe("AcpAdapterV2", () => {
6875 },6810 },
6876 });6811 });
6877 yield* Deferred.succeed(releasePromptCompletion, undefined);6812 yield* Deferred.succeed(releasePromptCompletion, undefined);
6878 yield* Effect.yieldNow;6813 yield* Deferred.await(promptSettled);
6879 yield* Effect.yieldNow;
68806814
6881 const firstProviderTurnId = idAllocator.derive.providerTurn({6815 const firstProviderTurnId = idAllocator.derive.providerTurn({
6882 driver: ACP_TEST_DRIVER,6816 driver: ACP_TEST_DRIVER,
6883 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),6817 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),
6884 });6818 });
6885 const interruptFiber = yield* runtime6819 yield* runtime.interruptTurn({ providerThread, providerTurnId: firstProviderTurnId });
6886 .interruptTurn({ providerThread, providerTurnId: firstProviderTurnId })
6887 .pipe(Effect.forkScoped);
6888 yield* TestClock.adjust("10 seconds");
6889 yield* Fiber.join(interruptFiber);
68906820
6891 let firstTerminalStatus: string | null = null;6821 let firstTerminalStatus: string | null = null;
6892 while (firstTerminalStatus === null) {6822 while (firstTerminalStatus === null) {
...@@ -7223,7 +7153,6 @@ describe("AcpAdapterV2", () => {...@@ -7223,7 +7153,6 @@ describe("AcpAdapterV2", () => {
7223 const mockAgentPath = yield* path.fromFileUrl(7153 const mockAgentPath = yield* path.fromFileUrl(
7224 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),7154 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
7225 );7155 );
7226 const protocolEvents = yield* Queue.bounded<EffectAcpProtocol.AcpProtocolLogEvent>(256);
7227 const continuationRequests: Array<ProviderContinuationRequest> = [];7156 const continuationRequests: Array<ProviderContinuationRequest> = [];
7228 const instanceId = ProviderInstanceId.make("acp-test");7157 const instanceId = ProviderInstanceId.make("acp-test");
7229 const childSessionId = "mock-child-session-pending-continuation";7158 const childSessionId = "mock-child-session-pending-continuation";
...@@ -7231,7 +7160,12 @@ describe("AcpAdapterV2", () => {...@@ -7231,7 +7160,12 @@ describe("AcpAdapterV2", () => {
7231 let subagentPhase: "spawn" | "complete" = "spawn";7160 let subagentPhase: "spawn" | "complete" = "spawn";
7232 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];7161 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];
7233 let sessionUpdateHandler: Parameters<RuntimeService["handleSessionUpdate"]>[0] | undefined;7162 let sessionUpdateHandler: Parameters<RuntimeService["handleSessionUpdate"]>[0] | undefined;
7163 const promptSettled = yield* Deferred.make<void>();
7234 const adapter = makeAcpAdapterV2({7164 const adapter = makeAcpAdapterV2({
7165 testHooks: {
7166 afterPromptSettledWithBackgroundWork: () =>
7167 Deferred.succeed(promptSettled, undefined).pipe(Effect.asVoid),
7168 },
7235 crypto: yield* Crypto.Crypto,7169 crypto: yield* Crypto.Crypto,
7236 instanceId,7170 instanceId,
7237 flavor: {7171 flavor: {
...@@ -7265,7 +7199,6 @@ describe("AcpAdapterV2", () => {...@@ -7265,7 +7199,6 @@ describe("AcpAdapterV2", () => {
7265 childProcessSpawner,7199 childProcessSpawner,
7266 mockAgentPath,7200 mockAgentPath,
7267 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },7201 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },
7268 protocolEvents,
7269 wrapRuntime: (runtime) => ({7202 wrapRuntime: (runtime) => ({
7270 ...runtime,7203 ...runtime,
7271 handleSessionUpdate: (handler) =>7204 handleSessionUpdate: (handler) =>
...@@ -7321,28 +7254,13 @@ describe("AcpAdapterV2", () => {...@@ -7321,28 +7254,13 @@ describe("AcpAdapterV2", () => {
7321 yield* runtime.startTurn(7254 yield* runtime.startTurn(
7322 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),7255 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),
7323 );7256 );
7324 yield* Stream.fromQueue(protocolEvents).pipe(7257 yield* Deferred.await(promptSettled);
7325 Stream.filter(
7326 (event) =>
7327 event.direction === "incoming" &&
7328 event.stage === "raw" &&
7329 typeof event.payload === "string" &&
7330 event.payload.includes('"stopReason"'),
7331 ),
7332 Stream.runHead,
7333 );
7334 yield* Effect.yieldNow;
7335 yield* Effect.yieldNow;
73367258
7337 const firstProviderTurnId = idAllocator.derive.providerTurn({7259 const firstProviderTurnId = idAllocator.derive.providerTurn({
7338 driver: ACP_TEST_DRIVER,7260 driver: ACP_TEST_DRIVER,
7339 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),7261 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),
7340 });7262 });
7341 const interruptFiber = yield* runtime7263 yield* runtime.interruptTurn({ providerThread, providerTurnId: firstProviderTurnId });
7342 .interruptTurn({ providerThread, providerTurnId: firstProviderTurnId })
7343 .pipe(Effect.forkScoped);
7344 yield* TestClock.adjust("10 seconds");
7345 yield* Fiber.join(interruptFiber);
73467264
7347 let firstTerminalStatus: string | null = null;7265 let firstTerminalStatus: string | null = null;
7348 while (firstTerminalStatus === null) {7266 while (firstTerminalStatus === null) {
...@@ -7506,14 +7424,18 @@ describe("AcpAdapterV2", () => {...@@ -7506,14 +7424,18 @@ describe("AcpAdapterV2", () => {
7506 const mockAgentPath = yield* path.fromFileUrl(7424 const mockAgentPath = yield* path.fromFileUrl(
7507 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),7425 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
7508 );7426 );
7509 const protocolEvents = yield* Queue.bounded<EffectAcpProtocol.AcpProtocolLogEvent>(256);
7510 const continuationRequests: Array<ProviderContinuationRequest> = [];7427 const continuationRequests: Array<ProviderContinuationRequest> = [];
7511 const instanceId = ProviderInstanceId.make("acp-test");7428 const instanceId = ProviderInstanceId.make("acp-test");
7512 const childSessionId = "mock-child-session-root-then-child";7429 const childSessionId = "mock-child-session-root-then-child";
7513 let subagentPhase: "spawn" | "complete" = "spawn";7430 let subagentPhase: "spawn" | "complete" = "spawn";
7514 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];7431 type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"];
7515 let sessionUpdateHandler: Parameters<RuntimeService["handleSessionUpdate"]>[0] | undefined;7432 let sessionUpdateHandler: Parameters<RuntimeService["handleSessionUpdate"]>[0] | undefined;
7433 const promptSettled = yield* Deferred.make<void>();
7516 const adapter = makeAcpAdapterV2({7434 const adapter = makeAcpAdapterV2({
7435 testHooks: {
7436 afterPromptSettledWithBackgroundWork: () =>
7437 Deferred.succeed(promptSettled, undefined).pipe(Effect.asVoid),
7438 },
7517 crypto: yield* Crypto.Crypto,7439 crypto: yield* Crypto.Crypto,
7518 instanceId,7440 instanceId,
7519 flavor: {7441 flavor: {
...@@ -7547,7 +7469,6 @@ describe("AcpAdapterV2", () => {...@@ -7547,7 +7469,6 @@ describe("AcpAdapterV2", () => {
7547 childProcessSpawner,7469 childProcessSpawner,
7548 mockAgentPath,7470 mockAgentPath,
7549 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },7471 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },
7550 protocolEvents,
7551 wrapRuntime: (runtime) => ({7472 wrapRuntime: (runtime) => ({
7552 ...runtime,7473 ...runtime,
7553 handleSessionUpdate: (handler) =>7474 handleSessionUpdate: (handler) =>
...@@ -7603,28 +7524,13 @@ describe("AcpAdapterV2", () => {...@@ -7603,28 +7524,13 @@ describe("AcpAdapterV2", () => {
7603 yield* runtime.startTurn(7524 yield* runtime.startTurn(
7604 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),7525 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),
7605 );7526 );
7606 yield* Stream.fromQueue(protocolEvents).pipe(7527 yield* Deferred.await(promptSettled);
7607 Stream.filter(
7608 (event) =>
7609 event.direction === "incoming" &&
7610 event.stage === "raw" &&
7611 typeof event.payload === "string" &&
7612 event.payload.includes('"stopReason"'),
7613 ),
7614 Stream.runHead,
7615 );
7616 yield* Effect.yieldNow;
7617 yield* Effect.yieldNow;
76187528
7619 const firstProviderTurnId = idAllocator.derive.providerTurn({7529 const firstProviderTurnId = idAllocator.derive.providerTurn({
7620 driver: ACP_TEST_DRIVER,7530 driver: ACP_TEST_DRIVER,
7621 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),7531 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),
7622 });7532 });
7623 const interruptFiber = yield* runtime7533 yield* runtime.interruptTurn({ providerThread, providerTurnId: firstProviderTurnId });
7624 .interruptTurn({ providerThread, providerTurnId: firstProviderTurnId })
7625 .pipe(Effect.forkScoped);
7626 yield* TestClock.adjust("10 seconds");
7627 yield* Fiber.join(interruptFiber);
76287534
7629 let firstTerminalStatus: string | null = null;7535 let firstTerminalStatus: string | null = null;
7630 while (firstTerminalStatus === null) {7536 while (firstTerminalStatus === null) {
...@@ -7760,12 +7666,16 @@ describe("AcpAdapterV2", () => {...@@ -7760,12 +7666,16 @@ describe("AcpAdapterV2", () => {
7760 const mockAgentPath = yield* path.fromFileUrl(7666 const mockAgentPath = yield* path.fromFileUrl(
7761 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),7667 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
7762 );7668 );
7763 const protocolEvents = yield* Queue.bounded<EffectAcpProtocol.AcpProtocolLogEvent>(256);
7764 const instanceId = ProviderInstanceId.make("acp-test");7669 const instanceId = ProviderInstanceId.make("acp-test");
7765 let subagentPhase: "spawn" | "complete" = "spawn";7670 let subagentPhase: "spawn" | "complete" = "spawn";
7766 let cancelCalled = false;7671 let cancelCalled = false;
7767 let runtimeOrdinalSeen = 0;7672 let runtimeOrdinalSeen = 0;
7673 const promptSettled = yield* Deferred.make<void>();
7768 const adapter = makeAcpAdapterV2({7674 const adapter = makeAcpAdapterV2({
7675 testHooks: {
7676 afterPromptSettledWithBackgroundWork: () =>
7677 Deferred.succeed(promptSettled, undefined).pipe(Effect.asVoid),
7678 },
7769 crypto: yield* Crypto.Crypto,7679 crypto: yield* Crypto.Crypto,
7770 instanceId,7680 instanceId,
7771 flavor: {7681 flavor: {
...@@ -7812,7 +7722,6 @@ describe("AcpAdapterV2", () => {...@@ -7812,7 +7722,6 @@ describe("AcpAdapterV2", () => {
7812 runtimeOrdinalSeen = Math.max(runtimeOrdinalSeen, runtimeOrdinal);7722 runtimeOrdinalSeen = Math.max(runtimeOrdinalSeen, runtimeOrdinal);
7813 return { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" };7723 return { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" };
7814 },7724 },
7815 protocolEvents,
7816 wrapCancel: (cancel) =>7725 wrapCancel: (cancel) =>
7817 Effect.sync(() => {7726 Effect.sync(() => {
7818 cancelCalled = true;7727 cancelCalled = true;
...@@ -7853,28 +7762,13 @@ describe("AcpAdapterV2", () => {...@@ -7853,28 +7762,13 @@ describe("AcpAdapterV2", () => {
7853 );7762 );
7854 // The still-running subagent defers finalize after session/prompt returns,7763 // The still-running subagent defers finalize after session/prompt returns,
7855 // so the interrupt below hits a settled turn held open for background work.7764 // so the interrupt below hits a settled turn held open for background work.
7856 yield* Stream.fromQueue(protocolEvents).pipe(7765 yield* Deferred.await(promptSettled);
7857 Stream.filter(
7858 (event) =>
7859 event.direction === "incoming" &&
7860 event.stage === "raw" &&
7861 typeof event.payload === "string" &&
7862 event.payload.includes('"stopReason"'),
7863 ),
7864 Stream.runHead,
7865 );
7866 yield* Effect.yieldNow;
7867 yield* Effect.yieldNow;
78687766
7869 const firstProviderTurnId = idAllocator.derive.providerTurn({7767 const firstProviderTurnId = idAllocator.derive.providerTurn({
7870 driver: ACP_TEST_DRIVER,7768 driver: ACP_TEST_DRIVER,
7871 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),7769 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),
7872 });7770 });
7873 const interruptFiber = yield* runtime7771 yield* runtime.interruptTurn({ providerThread, providerTurnId: firstProviderTurnId });
7874 .interruptTurn({ providerThread, providerTurnId: firstProviderTurnId })
7875 .pipe(Effect.forkScoped);
7876 yield* TestClock.adjust("10 seconds");
7877 yield* Fiber.join(interruptFiber);
7878 assert.isFalse(7772 assert.isFalse(
7879 cancelCalled,7773 cancelCalled,
7880 "settled soft steer must not send session/cancel (the real Grok CLI kills background subagents on cancel)",7774 "settled soft steer must not send session/cancel (the real Grok CLI kills background subagents on cancel)",
...@@ -13725,10 +13619,14 @@ describe("AcpAdapterV2", () => {...@@ -13725,10 +13619,14 @@ describe("AcpAdapterV2", () => {
13725 const mockAgentPath = yield* path.fromFileUrl(13619 const mockAgentPath = yield* path.fromFileUrl(
13726 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),13620 new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
13727 );13621 );
13728 const protocolEvents = yield* Queue.bounded<EffectAcpProtocol.AcpProtocolLogEvent>(256);
13729 const instanceId = ProviderInstanceId.make("acp-test");13622 const instanceId = ProviderInstanceId.make("acp-test");
13730 let subagentPhase: "spawn" | "complete" = "spawn";13623 let subagentPhase: "spawn" | "complete" = "spawn";
13624 const promptSettled = yield* Deferred.make<void>();
13731 const adapter = makeAcpAdapterV2({13625 const adapter = makeAcpAdapterV2({
13626 testHooks: {
13627 afterPromptSettledWithBackgroundWork: () =>
13628 Deferred.succeed(promptSettled, undefined).pipe(Effect.asVoid),
13629 },
13732 crypto: yield* Crypto.Crypto,13630 crypto: yield* Crypto.Crypto,
13733 instanceId,13631 instanceId,
13734 flavor: {13632 flavor: {
...@@ -13762,7 +13660,6 @@ describe("AcpAdapterV2", () => {...@@ -13762,7 +13660,6 @@ describe("AcpAdapterV2", () => {
13762 childProcessSpawner,13660 childProcessSpawner,
13763 mockAgentPath,13661 mockAgentPath,
13764 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },13662 environment: { T3_ACP_EMIT_GENERIC_TOOL_PLACEHOLDERS: "1" },
13765 protocolEvents,
13766 }),13663 }),
13767 },13664 },
13768 fileSystem,13665 fileSystem,
...@@ -13799,32 +13696,17 @@ describe("AcpAdapterV2", () => {...@@ -13799,32 +13696,17 @@ describe("AcpAdapterV2", () => {
13799 yield* runtime.startTurn(13696 yield* runtime.startTurn(
13800 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),13697 makeTurnInput({ threadId, providerThread, instanceId, runtimePolicy, now }),
13801 );13698 );
13802 yield* Stream.fromQueue(protocolEvents).pipe(13699 yield* Deferred.await(promptSettled);
13803 Stream.filter(
13804 (event) =>
13805 event.direction === "incoming" &&
13806 event.stage === "raw" &&
13807 typeof event.payload === "string" &&
13808 event.payload.includes('"stopReason"'),
13809 ),
13810 Stream.runHead,
13811 );
13812 yield* Effect.yieldNow;
13813 yield* Effect.yieldNow;
1381413700
13815 const firstProviderTurnId = idAllocator.derive.providerTurn({13701 const firstProviderTurnId = idAllocator.derive.providerTurn({
13816 driver: ACP_TEST_DRIVER,13702 driver: ACP_TEST_DRIVER,
13817 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),13703 nativeTurnId: acpScopedNativeId(instanceId, "mock-session-1:turn:1"),
13818 });13704 });
13819 const interruptFiber = yield* runtime13705 yield* runtime.interruptTurn({
13820 .interruptTurn({13706 providerThread,
13821 providerThread,13707 providerTurnId: firstProviderTurnId,
13822 providerTurnId: firstProviderTurnId,13708 requestRuntimeRestart: true,
13823 requestRuntimeRestart: true,13709 });
13824 })
13825 .pipe(Effect.forkScoped);
13826 yield* TestClock.adjust("10 seconds");
13827 yield* Fiber.join(interruptFiber);
1382813710
13829 let subagentStatus: string | null = null;13711 let subagentStatus: string | null = null;
13830 let firstTerminalStatus: string | null = null;13712 let firstTerminalStatus: string | null = null;