authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-10-11 00:46:57+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-10 17:46:57-05:00
log7e8071f1f1566220fb52cbfaaf97a2bc4e93a193
tree8ee4b159160fb59adb11f6413f5d1b18f2d23dcc
parente408cac0d7d903ccfcecfb906d4e48292294ab55
signature Signed by PGP key B5690EEEBB952194

fix(pi): preserve native abort outcomes (#17853)


2 files changed, 189 insertions(+), 98 deletions(-)

packages/provider-pi/src/server/adapter.test.ts+177-89
......@@ -1045,97 +1045,112 @@ describe("PiAdapterV2", () => {
10451045 }).pipe(Effect.scoped, Effect.provide(layerTest)),
10461046 );
10471047
1048 it.effect("defers a wake's late settlement until the joined user prompt is acknowledged", () =>
1049 Effect.gen(function* () {
1050 const fake = yield* makeFakePi;
1051 const offers =
1052 yield* Queue.unbounded<ProviderContinuationRequests.ProviderContinuationRequest>();
1053 const { runtime, takeEvent } = yield* openRuntime(
1054 fake,
1055 "default",
1056 THREAD_ID,
1057 SESSION_ID,
1058 undefined,
1059 {
1060 offer: (request) => Queue.offer(offers, request).pipe(Effect.asVoid),
1061 },
1062 );
1063 const providerThread = yield* runtime.ensureThread({
1064 threadId: THREAD_ID,
1065 modelSelection: modelSelection("default"),
1066 runtimePolicy,
1067 });
1068 yield* fake.emit({ type: "agent_start" });
1069 yield* Queue.take(offers);
1070 yield* startTurn(runtime, providerThread);
1071 yield* fake.takeRequest("prompt");
1072 yield* fake.emit({ type: "agent_settled" });
1073 yield* fake.emit({
1074 type: "extension_ui_request",
1075 method: "notify",
1076 message: "Ordering barrier",
1077 });
1078 yield* takeEvent(
1079 (event) =>
1080 event.type === "turn_item.updated" &&
1081 event.turnItem.type === "dynamic_tool" &&
1082 event.turnItem.toolName === "notify",
1083 );
1084 assert.equal(runtime.providerSession.status, "running");
1085 fake.queueState({ isStreaming: true });
1086 yield* fake.emit({ type: "response", command: "prompt", success: true });
1087 yield* fake.takeRequest("get_state");
1088 yield* fake.emit({ type: "agent_start" });
1089 yield* fake.emit({ type: "agent_settled" });
1090 yield* takeEvent((event) => event.type === "turn.terminal");
1091 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1048 it.effect.each([false, true])(
1049 "defers a wake's late settlement until the joined user prompt is acknowledged, aborted=%s",
1050 (aborted) =>
1051 Effect.gen(function* () {
1052 const fake = yield* makeFakePi;
1053 const offers =
1054 yield* Queue.unbounded<ProviderContinuationRequests.ProviderContinuationRequest>();
1055 const { runtime, takeEvent } = yield* openRuntime(
1056 fake,
1057 "default",
1058 THREAD_ID,
1059 SESSION_ID,
1060 undefined,
1061 {
1062 offer: (request) => Queue.offer(offers, request).pipe(Effect.asVoid),
1063 },
1064 );
1065 const providerThread = yield* runtime.ensureThread({
1066 threadId: THREAD_ID,
1067 modelSelection: modelSelection("default"),
1068 runtimePolicy,
1069 });
1070 yield* fake.emit({ type: "agent_start" });
1071 yield* Queue.take(offers);
1072 yield* startTurn(runtime, providerThread);
1073 yield* fake.takeRequest("prompt");
1074 yield* fake.emit({ type: "agent_settled", aborted });
1075 yield* fake.emit({
1076 type: "extension_ui_request",
1077 method: "notify",
1078 message: "Ordering barrier",
1079 });
1080 yield* takeEvent(
1081 (event) =>
1082 event.type === "turn_item.updated" &&
1083 event.turnItem.type === "dynamic_tool" &&
1084 event.turnItem.toolName === "notify",
1085 );
1086 assert.equal(runtime.providerSession.status, "running");
1087 fake.queueState({ isStreaming: true });
1088 yield* fake.emit({ type: "response", command: "prompt", success: true });
1089 yield* fake.takeRequest("get_state");
1090 yield* fake.emit({ type: "agent_start" });
1091 yield* fake.emit({ type: "agent_settled" });
1092 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");
1093 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "completed");
1094 }).pipe(Effect.scoped, Effect.provide(layerTest)),
10921095 );
10931096
1094 it.effect("settles a command-only user prompt after taking a completed wake", () =>
1095 Effect.gen(function* () {
1096 const fake = yield* makeFakePi;
1097 const offers =
1098 yield* Queue.unbounded<ProviderContinuationRequests.ProviderContinuationRequest>();
1099 const { runtime, takeEvent } = yield* openRuntime(
1100 fake,
1101 "default",
1102 THREAD_ID,
1103 SESSION_ID,
1104 undefined,
1105 {
1106 offer: (request) => Queue.offer(offers, request).pipe(Effect.asVoid),
1107 },
1108 );
1109 const providerThread = yield* runtime.ensureThread({
1110 threadId: THREAD_ID,
1111 modelSelection: modelSelection("default"),
1112 runtimePolicy,
1113 });
1114 yield* fake.emit({ type: "agent_start" });
1115 yield* Queue.take(offers);
1116 yield* fake.emit({ type: "agent_settled" });
1117 yield* takeEvent(
1118 (event) =>
1119 event.type === "provider_session.updated" && event.providerSession.status === "ready",
1120 );
1121 yield* startTurn(runtime, providerThread, "default", [], "/hello");
1122 yield* fake.takeRequest("prompt");
1123 yield* fake.emit({ type: "response", command: "prompt", success: true });
1124 yield* takeEvent((event) => event.type === "turn.terminal");
1125 yield* startTurn(
1126 runtime,
1127 providerThread,
1128 "default",
1129 [],
1130 "Stale continuation",
1131 undefined,
1132 2,
1133 THREAD_ID,
1134 true,
1135 );
1136 yield* takeEvent((event) => event.type === "turn.terminal");
1137 assert.equal(fake.allRequests().filter((request) => request["type"] === "prompt").length, 1);
1138 }).pipe(Effect.scoped, Effect.provide(layerTest)),
1097 it.effect.each([
1098 { aborted: false, resumes: false, expected: "completed" },
1099 { aborted: true, resumes: false, expected: "interrupted" },
1100 { aborted: true, resumes: true, expected: "completed" },
1101 ])(
1102 "settles a completed wake, aborted=$aborted, resumes=$resumes",
1103 ({ aborted, resumes, expected }) =>
1104 Effect.gen(function* () {
1105 const fake = yield* makeFakePi;
1106 const offers =
1107 yield* Queue.unbounded<ProviderContinuationRequests.ProviderContinuationRequest>();
1108 const { runtime, takeEvent } = yield* openRuntime(
1109 fake,
1110 "default",
1111 THREAD_ID,
1112 SESSION_ID,
1113 undefined,
1114 {
1115 offer: (request) => Queue.offer(offers, request).pipe(Effect.asVoid),
1116 },
1117 );
1118 const providerThread = yield* runtime.ensureThread({
1119 threadId: THREAD_ID,
1120 modelSelection: modelSelection("default"),
1121 runtimePolicy,
1122 });
1123 yield* fake.emit({ type: "agent_start" });
1124 yield* Queue.take(offers);
1125 yield* fake.emit({ type: "agent_settled", aborted });
1126 yield* takeEvent(
1127 (event) =>
1128 event.type === "provider_session.updated" && event.providerSession.status === "ready",
1129 );
1130 yield* startTurn(runtime, providerThread, "default", [], "/hello");
1131 yield* fake.takeRequest("prompt");
1132 if (resumes) yield* fake.emit({ type: "agent_start" });
1133 yield* fake.emit({ type: "response", command: "prompt", success: true });
1134 if (resumes) yield* fake.emit({ type: "agent_settled", aborted: false });
1135 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");
1136 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === expected);
1137 yield* startTurn(
1138 runtime,
1139 providerThread,
1140 "default",
1141 [],
1142 "Stale continuation",
1143 undefined,
1144 2,
1145 THREAD_ID,
1146 true,
1147 );
1148 yield* takeEvent((event) => event.type === "turn.terminal");
1149 assert.equal(
1150 fake.allRequests().filter((request) => request["type"] === "prompt").length,
1151 1,
1152 );
1153 }).pipe(Effect.scoped, Effect.provide(layerTest)),
11391154 );
11401155
11411156 it.effect("keeps a pending wake on its thread and lets Stop retire its process", () =>
......@@ -3508,6 +3523,79 @@ describe("PiAdapterV2", () => {
35083523 }).pipe(Effect.scoped, Effect.provide(layerTest)),
35093524 );
35103525
3526 it.effect.each([undefined, false, true])(
3527 "preserves the native settlement outcome, aborted=%s",
3528 (aborted) =>
3529 Effect.gen(function* () {
3530 const fake = yield* makeFakePi;
3531 const { runtime, takeEvent } = yield* openRuntime(fake);
3532 const providerThread = yield* runtime.ensureThread({
3533 threadId: THREAD_ID,
3534 modelSelection: modelSelection("default"),
3535 runtimePolicy,
3536 });
3537 yield* startTurn(runtime, providerThread);
3538 yield* fake.takeRequest("prompt");
3539 yield* fake.emit({ type: "agent_start" });
3540 yield* fake.emit({
3541 type: "agent_settled",
3542 ...(aborted === undefined ? {} : { aborted }),
3543 });
3544 const completed = yield* takeEvent(
3545 (event) =>
3546 event.type === "provider_turn.updated" && event.providerTurn.status !== "running",
3547 );
3548 const status = aborted === true ? "interrupted" : "completed";
3549 assert.isTrue(
3550 completed.type === "provider_turn.updated" && completed.providerTurn.status === status,
3551 );
3552 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");
3553 assert.isTrue(
3554 terminal.type === "turn.terminal" &&
3555 terminal.status === status &&
3556 terminal.failure === null,
3557 );
3558 assert.equal(runtime.providerSession.status, "ready");
3559 }).pipe(Effect.scoped, Effect.provide(layerTest)),
3560 );
3561
3562 it.effect("stops retry progress when Pi aborts the native run", () =>
3563 Effect.gen(function* () {
3564 const fake = yield* makeFakePi;
3565 const { runtime, takeEvent } = yield* openRuntime(fake);
3566 const providerThread = yield* runtime.ensureThread({
3567 threadId: THREAD_ID,
3568 modelSelection: modelSelection("default"),
3569 runtimePolicy,
3570 });
3571 yield* startTurn(runtime, providerThread);
3572 yield* fake.takeRequest("prompt");
3573 yield* fake.emit({ type: "agent_start" });
3574 yield* fake.emit({
3575 type: "auto_retry_start",
3576 attempt: 2,
3577 maxAttempts: 5,
3578 delayMs: 6_000,
3579 errorMessage: "temporary network failure",
3580 });
3581 const retrying = yield* takeEvent(
3582 (event) => event.type === "turn_item.updated" && event.turnItem.type === "error",
3583 );
3584 yield* fake.emit({ type: "agent_settled", aborted: true });
3585 const stopped = yield* takeEvent(
3586 (event) => event.type === "turn_item.updated" && event.turnItem.type === "error",
3587 );
3588 assert.isTrue(
3589 retrying.type === "turn_item.updated" &&
3590 stopped.type === "turn_item.updated" &&
3591 stopped.turnItem.id === retrying.turnItem.id &&
3592 stopped.turnItem.status === "interrupted",
3593 );
3594 const terminal = yield* takeEvent((event) => event.type === "turn.terminal");
3595 assert.isTrue(terminal.type === "turn.terminal" && terminal.status === "interrupted");
3596 }).pipe(Effect.scoped, Effect.provide(layerTest)),
3597 );
3598
35113599 it.effect("stops active retry progress when the turn is interrupted", () =>
35123600 Effect.gen(function* () {
35133601 const fake = yield* makeFakePi;
packages/provider-pi/src/server/adapter.ts+12-9
......@@ -410,6 +410,8 @@ interface ActivePiTurn {
410410 */
411411 readonly toolStartedAt: Map<string, DateTime.Utc>;
412412 interrupted: boolean;
413 /** Cancellation of the latest native run; new work can recover before settlement. */
414 nativeAborted: boolean;
413415 /**
414416 * Whether any agent run activity was observed. Command-only prompts (pure
415417 * extension slash commands) never start an agent run and never emit
......@@ -1574,19 +1576,16 @@ export const makePiAdapterV2 = Effect.fn("makePiAdapterV2")(function* (
15741576 const turn = state.activeTurn;
15751577 if (turn === null) return;
15761578 state.activeTurn = null;
1579 const interrupted = turn.interrupted || turn.nativeAborted;
15771580 const completedAt = yield* DateTime.now;
15781581 yield* completeOpenStreamItems(turn);
15791582 if (turn.activeCompaction !== null) {
1580 const status = turn.interrupted
1581 ? "cancelled"
1582 : turn.failure === null
1583 ? "completed"
1584 : "failed";
1583 const status = interrupted ? "cancelled" : turn.failure === null ? "completed" : "failed";
15851584 yield* emitCompaction(turn, turn.activeCompaction, status);
15861585 turn.activeCompaction = null;
15871586 }
15881587 if (turn.activeProviderRetry !== null) {
1589 if (turn.interrupted) {
1588 if (interrupted) {
15901589 yield* emitProviderRetry(turn, turn.activeProviderRetry, "interrupted", completedAt);
15911590 turn.activeProviderRetry = null;
15921591 } else if (turn.failure === null) {
......@@ -1600,7 +1599,7 @@ export const makePiAdapterV2 = Effect.fn("makePiAdapterV2")(function* (
16001599 const tokenUsage = readUsage
16011600 ? yield* readTokenUsage(turn.latestCompactionAfterTokens, completedAt)
16021601 : undefined;
1603 const failure = turn.interrupted ? null : (turn.rejectedPromptFailure ?? turn.failure);
1602 const failure = interrupted ? null : (turn.rejectedPromptFailure ?? turn.failure);
16041603 yield* emit({
16051604 type: "provider_turn.updated",
16061605 driver: PI_PROVIDER,
......@@ -1610,7 +1609,7 @@ export const makePiAdapterV2 = Effect.fn("makePiAdapterV2")(function* (
16101609 ...(treeRefs === null || treeRefs.nativeTurnRef === undefined
16111610 ? {}
16121611 : { nativeTurnRef: treeRefs.nativeTurnRef }),
1613 status: turn.interrupted ? "interrupted" : failure !== null ? "failed" : "completed",
1612 status: interrupted ? "interrupted" : failure !== null ? "failed" : "completed",
16141613 completedAt,
16151614 ...(tokenUsage === undefined ? {} : { tokenUsage }),
16161615 },
......@@ -1679,7 +1678,7 @@ export const makePiAdapterV2 = Effect.fn("makePiAdapterV2")(function* (
16791678 providerThreadId: state.providerThread.id,
16801679 providerTurnId: turn.providerTurn.id,
16811680 runOrdinal: turn.turnInput.runOrdinal,
1682 status: turn.interrupted ? "interrupted" : "completed",
1681 status: interrupted ? "interrupted" : "completed",
16831682 failure: null,
16841683 threadDisposition: "reusable",
16851684 });
......@@ -1799,6 +1798,7 @@ export const makePiAdapterV2 = Effect.fn("makePiAdapterV2")(function* (
17991798 return;
18001799 }
18011800 turn.sawAgentActivity = true;
1801 turn.nativeAborted = false;
18021802 turn.settleProbeGeneration += 1;
18031803 return;
18041804 }
......@@ -2018,6 +2018,7 @@ export const makePiAdapterV2 = Effect.fn("makePiAdapterV2")(function* (
20182018 return;
20192019 }
20202020 if (turn !== null) {
2021 turn.nativeAborted = event["aborted"] === true;
20212022 turn.settleWhenIdle = true;
20222023 turn.settleProbeGeneration += 1;
20232024 // A wake can settle while the newly joined user prompt is still
......@@ -2610,6 +2611,7 @@ export const makePiAdapterV2 = Effect.fn("makePiAdapterV2")(function* (
26102611 toolArgs: new Map(),
26112612 toolStartedAt: new Map(),
26122613 interrupted: false,
2614 nativeAborted: false,
26132615 sawAgentActivity: false,
26142616 adoptedWake: false,
26152617 promptMayBeCommandOnly:
......@@ -2682,6 +2684,7 @@ export const makePiAdapterV2 = Effect.fn("makePiAdapterV2")(function* (
26822684 if (wake !== null) {
26832685 for (const event of wake.events) {
26842686 if (!continuation && event["type"] === "agent_settled") {
2687 activeTurn.nativeAborted = event["aborted"] === true;
26852688 activeTurn.settleWhenIdle = true;
26862689 continue;
26872690 }