diff --git a/packages/cli/src/run/noninteractive.ts b/packages/cli/src/run/noninteractive.ts index 5043b28ee5e..f961efdf00b 100644 --- a/packages/cli/src/run/noninteractive.ts +++ b/packages/cli/src/run/noninteractive.ts @@ -135,7 +135,29 @@ export async function runNonInteractivePrompt(input: Input) { } } - const replyPermission = async (request: { id: string; action: string; resources: ReadonlyArray }) => { + // Subagents run in child sessions; their asks and questions belong to this run too. Other + // sessions on a shared server (e.g. the TUI's) must be left alone. + const owned = new Map>([[input.sessionID, Promise.resolve(true)]]) + const ownsSession = (sessionID: string): Promise => { + const known = owned.get(sessionID) + if (known) return known + const result = + sessionID === GLOBAL_FORM_SESSION_ID + ? Promise.resolve(false) + : input.client.session + .get({ sessionID }) + .then((session) => (session.parentID ? ownsSession(session.parentID) : false)) + .catch(() => false) + owned.set(sessionID, result) + return result + } + + const replyPermission = async (request: { + id: string + sessionID: string + action: string + resources: ReadonlyArray + }) => { // Nobody can approve here. Outside V1 compatibility, reject with feedback so the tool fails // as ordinary model-visible output and the model continues without the action. const continuing = !input.auto && input.compatibility !== "v1" @@ -149,7 +171,7 @@ export async function runNonInteractivePrompt(input: Input) { } await input.client.permission .reply({ - sessionID: input.sessionID, + sessionID: request.sessionID, requestID: request.id, decision: input.auto ? "once" : "reject", ...(continuing ? { message: PERMISSION_REJECTED_FEEDBACK } : {}), @@ -185,14 +207,14 @@ export async function runNonInteractivePrompt(input: Input) { } const event = next.value - if (event.type === "permission.asked" && submitted && event.data.sessionID === input.sessionID) { + if (event.type === "permission.asked" && submitted && (await ownsSession(event.data.sessionID))) { await replyPermission(event.data) continue } if ( event.type === "form.created" && submitted && - (event.data.form.sessionID === input.sessionID || + ((await ownsSession(event.data.form.sessionID)) || (!input.attached && event.data.form.sessionID === GLOBAL_FORM_SESSION_ID && sameLocation(event.location, input.location))) diff --git a/packages/core/src/permission.ts b/packages/core/src/permission.ts index e3586f573b4..2aecebb0ce3 100644 --- a/packages/core/src/permission.ts +++ b/packages/core/src/permission.ts @@ -286,7 +286,11 @@ const layer = Layer.effect( requestID: item.request.id, reply: "reject", }) - yield* Deferred.fail(item.deferred, new DeclinedError()) + // Feedback applies to the whole batch, so parallel asks don't end the step. + yield* Deferred.fail( + item.deferred, + input.message ? new CorrectedError({ feedback: input.message }) : new DeclinedError(), + ) pending.delete(id) } return