From cf84c73a8cf04f466f1bdd2853baa761b29b15a4 Mon Sep 17 00:00:00 2001 From: Jongmin Park Date: Sat, 7 Nov 2026 04:25:42 +0900 Subject: [PATCH] feat: accept a per-turn working directory on session/prompt via _meta.cwd ACP session/prompt has no cwd of its own, so a client that lets the user navigate folders between turns cannot move a live session: session/resume with a different cwd can be rejected with invalid_params. Clients can now pass _meta.cwd on session/prompt (v1 and v2). The adapter validates it as an absolute, existing directory before the turn-start slot is taken and rejects the prompt with invalid_params otherwise, forwards it as the cwd of the codex turn/start request (which overrides the working directory for this turn and subsequent turns), and follows it on the session state once the turn starts. turn/start now always re-pins the session cwd like the other thread-setting overrides; prompts without _meta.cwd keep the session directory, and unknown _meta keys stay ignored. --- README.md | 1 + docs/prompt-cwd.md | 45 ++++++++++ src/CodexAcpClient.ts | 3 + src/CodexAcpServer.ts | 46 +++++++++- .../data/cancel-v2-running-prompt.json | 1 + .../permissions-v2-plan-implementation.json | 2 + .../data/prompt-v2-inserted.json | 1 + .../prompt-v2-invalid-encrypted-content.json | 1 + .../data/prompt-v2-not-inserted.json | 1 + .../prompt-v2-plan-implementation-turn.json | 2 + ...rompt-v2-process-exit-after-insertion.json | 2 + .../prompt-v2-turn-error-after-insertion.json | 2 + .../prompt-v2-turn-failed-or-interrupted.json | 2 + .../data/prompt-v2-turn-start-rejected.json | 1 + ...ompt-v2-typed-failure-after-insertion.json | 1 + .../data/prompt-v2-v1-unchanged.json | 1 + .../data/send-attachments-turn-start.json | 1 + .../data/steering-v2-fallback-new-turn.json | 1 + .../data/steering-v2-injected-owned-turn.json | 1 + .../CodexACPAgent/prompt-cwd.test.ts | 86 +++++++++++++++++++ 20 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 docs/prompt-cwd.md create mode 100644 src/__tests__/CodexACPAgent/prompt-cwd.test.ts diff --git a/README.md b/README.md index e9b6cb596..cd40c24d6 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Use [OpenAI Codex](https://github.com/openai/codex) from [Agent Client Protocol] - Model, reasoning effort, fast mode, approval, sandbox mode, and configured [Codex permission profile](docs/permission-profiles.md) selection. - Concrete recommended model and reasoning-effort values through the opt-in [AIR recommended config values](docs/air-extensions.md#recommended-config-values) capability. - Text prompts, embedded context, images, resource links, and additional workspace directories. +- A per-turn working directory override on `session/prompt` through [`_meta.cwd`](docs/prompt-cwd.md). - Shell command, file change, [permission request](docs/air-extensions.md#permission-presentation), MCP tool call, terminal output, reasoning, plan, web search, image generation, image view, token usage, and review events. - Compact file changes through the negotiated [AIR diff patch extension](docs/air-extensions.md#diff-patch). - For AIR, one tool call shape with each fact in one field, as the [tool call contract](docs/air-extensions.md#tool-call-contract) defines. Other clients keep the earlier fields. diff --git a/docs/prompt-cwd.md b/docs/prompt-cwd.md new file mode 100644 index 000000000..656bda708 --- /dev/null +++ b/docs/prompt-cwd.md @@ -0,0 +1,45 @@ +# Per-turn working directory + +ACP `session/prompt` carries no working directory of its own: a session keeps the `cwd` it was +created or last loaded with, and neither ACP v1 nor v2 has an in-session primitive to move a live +session to another directory. A `session/resume` with a different `cwd` can be rejected with +`invalid_params`. Clients that let the user navigate folders between turns (a GUI file tree, a +workspace switcher) therefore cannot move a running session with the standard methods. + +## Request + +On `session/prompt`, a client may pass a per-turn working directory override through `_meta.cwd`: + +```json +{ + "sessionId": "thread-1", + "prompt": [{"type": "text", "text": "Where are the tests?"}], + "_meta": {"cwd": "/Users/me/other-project"} +} +``` + +Both ACP v1 and v2 accept it. A prompt without `_meta`, with `_meta: null`, or with any other +`_meta` keys behaves exactly as before; `_meta.cwd: null` counts as absent. + +## Validation + +The adapter validates `_meta.cwd` before the prompt takes its turn-start slot, so an invalid value +rejects the prompt with the standard `invalid_params` error (`-32602`) and nothing is sent to +Codex: + +- the value must be a non-empty string, +- an absolute POSIX or Windows path (never a file URI), +- an existing directory. + +## Effect + +The validated directory is sent as the `cwd` of the Codex `turn/start` request, which overrides +the working directory for this turn and subsequent turns. Once the turn starts, the adapter +updates the session's working directory, so the following turns, file citations, and turn diffs +resolve against it. `session/list` reflects the new directory through Codex's own thread cwd. + +## Client support + +Older adapters ignore unknown `_meta` keys, so sending `_meta.cwd` to one is harmless but has no +effect: the turn still runs in the session's original directory. A client that needs the override +should gate on the adapter version or detect the effect before relying on it. diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index ed7e6120f..f0e1f77c2 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -1405,6 +1405,9 @@ export class CodexAcpClient { return await this.codexClient.runTurn({ threadId: request.sessionId, input: input, + // Re-pin the thread working directory like the other overrides below; it applies to + // this turn and subsequent turns, so Codex follows the session's cwd. + cwd: cwd, approvalPolicy: agentMode.approvalPolicy, approvalsReviewer: agentMode.approvalsReviewer, ...(permissionProfileId diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 87a438872..a30c9c2fc 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -104,6 +104,8 @@ import { } from "./ModelConfigOption"; import type {TokenCount} from "./TokenCount"; import {PromptTokenUsage, toPromptUsage, ZERO_TOKEN_COUNT} from "./TokenCount"; +import {statSync} from "node:fs"; +import {isAbsoluteProtocolPath} from "./PathUtils"; import {CodexCommands} from "./CodexCommands"; import {SteeringQueue} from "./SteeringQueue"; import type {QuotaMeta} from "./QuotaMeta"; @@ -387,6 +389,34 @@ function stopReasonForUnownedTurn(status: TurnStatus): acp.StopReason { } } +/** The `_meta.cwd` per-turn working directory override on `session/prompt`. */ +function readMetaTurnCwd(meta?: Record | null): string | undefined { + const rawCwd = meta?.["cwd"]; + // The ACP type allows `_meta: null`; treat it and a null `cwd` like an absent override. + if (rawCwd === undefined || rawCwd === null) { + return undefined; + } + if (typeof rawCwd !== "string" || rawCwd.trim().length === 0) { + throw RequestError.invalidParams(undefined, "_meta.cwd must be a non-empty string"); + } + const turnCwd = rawCwd.trim(); + if (!isAbsoluteProtocolPath(turnCwd)) { + throw RequestError.invalidParams(undefined, "_meta.cwd must be an absolute path"); + } + // Unlike `session/new`, a prompt must not fail after it was queued or inserted, so the + // directory is checked here instead of being left to Codex's turn-start error. + let stats; + try { + stats = statSync(turnCwd); + } catch { + throw RequestError.invalidParams(undefined, `_meta.cwd is not an existing directory: ${turnCwd}`); + } + if (!stats.isDirectory()) { + throw RequestError.invalidParams(undefined, `_meta.cwd is not a directory: ${turnCwd}`); + } + return turnCwd; +} + /** * Approval/elicitation handlers for the baseline Codex turn tracker installed before any prompt * has run. They answer exactly as app-server already defaults to for a thread with no @@ -4857,6 +4887,9 @@ export class CodexAcpServer { reservation?: TurnStartReservation, ): Promise { assertPromptContentSupported(params); + // Validated before the turn-start slot is taken, so a bad `_meta.cwd` rejects the prompt + // without touching the session. + const turnCwd = readMetaTurnCwd(params._meta); this.assertInstructionsReady(params.sessionId); // Callers that need to gate additional checks (closing, canStart) atomically with the // turn-start slot acquire their own reservation and pass it in; otherwise this call is @@ -4867,7 +4900,7 @@ export class CodexAcpServer { await activeReservation.wait; } try { - return await this.promptAfterReservation(params, signal, onTurnStarted, insertion); + return await this.promptAfterReservation(params, signal, onTurnStarted, insertion, turnCwd); } finally { if (ownsReservation) { activeReservation.release(); @@ -4880,6 +4913,7 @@ export class CodexAcpServer { signal?: AbortSignal, onTurnStarted?: () => void, insertion?: UserMessageInsertion, + turnCwd?: string, ): Promise { if (this.providerUpdate !== null) { await this.waitForProviderUpdate(); @@ -4889,6 +4923,9 @@ export class CodexAcpServer { prompt: params.prompt, }); const sessionState = this.getSessionState(params.sessionId); + // The directory this prompt's turn runs in: the per-turn `_meta.cwd` override, or the + // session's cwd. Codex adopts it for subsequent turns too, so the session state follows. + const turnWorkingDirectory = turnCwd ?? sessionState.cwd; // Awaited only when a clear is sent, so a prompt with no held suggestion starts in the same tick. const clearing = sessionState.promptSuggestions?.clear(); if (clearing) await clearing; @@ -4897,7 +4934,7 @@ export class CodexAcpServer { : null; const agentFileChangeWorkspace = agentFileChangeReportRequest === null ? null - : captureAgentFileChangeWorkspace(sessionState.cwd, sessionState.additionalDirectories); + : captureAgentFileChangeWorkspace(turnWorkingDirectory, sessionState.additionalDirectories); let agentFileChangeReportTurnId: string | null = null; let agentFileChangeReportUnavailableReason: AgentFileChangeReportUnavailableReason = "providerError"; let promptWasCancelled = false; @@ -5207,9 +5244,12 @@ export class CodexAcpServer { modelId, serviceTier, disableSummary, - sessionState.cwd, + turnWorkingDirectory, sessionState.additionalDirectories, (turnId) => { + // The turn/start request carried the turn working directory, so Codex runs + // this turn and subsequent turns there; follow it once the turn is real. + sessionState.cwd = turnWorkingDirectory; const turn = {threadId: params.sessionId, turnId}; activePrompt.currentTurn = turn; insertion?.onTurnStarted?.(turn); diff --git a/src/__tests__/CodexACPAgent/data/cancel-v2-running-prompt.json b/src/__tests__/CodexACPAgent/data/cancel-v2-running-prompt.json index 031b133d9..518a6300a 100644 --- a/src/__tests__/CodexACPAgent/data/cancel-v2-running-prompt.json +++ b/src/__tests__/CodexACPAgent/data/cancel-v2-running-prompt.json @@ -10,6 +10,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { diff --git a/src/__tests__/CodexACPAgent/data/permissions-v2-plan-implementation.json b/src/__tests__/CodexACPAgent/data/permissions-v2-plan-implementation.json index 2986a2175..60987ba8e 100644 --- a/src/__tests__/CodexACPAgent/data/permissions-v2-plan-implementation.json +++ b/src/__tests__/CodexACPAgent/data/permissions-v2-plan-implementation.json @@ -10,6 +10,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { @@ -212,6 +213,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { diff --git a/src/__tests__/CodexACPAgent/data/prompt-v2-inserted.json b/src/__tests__/CodexACPAgent/data/prompt-v2-inserted.json index 7d1cd30d4..b5f679f03 100644 --- a/src/__tests__/CodexACPAgent/data/prompt-v2-inserted.json +++ b/src/__tests__/CodexACPAgent/data/prompt-v2-inserted.json @@ -10,6 +10,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { diff --git a/src/__tests__/CodexACPAgent/data/prompt-v2-invalid-encrypted-content.json b/src/__tests__/CodexACPAgent/data/prompt-v2-invalid-encrypted-content.json index 2ea58a5e2..50ef62a1c 100644 --- a/src/__tests__/CodexACPAgent/data/prompt-v2-invalid-encrypted-content.json +++ b/src/__tests__/CodexACPAgent/data/prompt-v2-invalid-encrypted-content.json @@ -10,6 +10,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { diff --git a/src/__tests__/CodexACPAgent/data/prompt-v2-not-inserted.json b/src/__tests__/CodexACPAgent/data/prompt-v2-not-inserted.json index df26b7fc8..8f4741830 100644 --- a/src/__tests__/CodexACPAgent/data/prompt-v2-not-inserted.json +++ b/src/__tests__/CodexACPAgent/data/prompt-v2-not-inserted.json @@ -10,6 +10,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { diff --git a/src/__tests__/CodexACPAgent/data/prompt-v2-plan-implementation-turn.json b/src/__tests__/CodexACPAgent/data/prompt-v2-plan-implementation-turn.json index ea226dc28..564138edd 100644 --- a/src/__tests__/CodexACPAgent/data/prompt-v2-plan-implementation-turn.json +++ b/src/__tests__/CodexACPAgent/data/prompt-v2-plan-implementation-turn.json @@ -10,6 +10,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { @@ -169,6 +170,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { diff --git a/src/__tests__/CodexACPAgent/data/prompt-v2-process-exit-after-insertion.json b/src/__tests__/CodexACPAgent/data/prompt-v2-process-exit-after-insertion.json index b805f424b..d218cd259 100644 --- a/src/__tests__/CodexACPAgent/data/prompt-v2-process-exit-after-insertion.json +++ b/src/__tests__/CodexACPAgent/data/prompt-v2-process-exit-after-insertion.json @@ -13,6 +13,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { @@ -102,6 +103,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { diff --git a/src/__tests__/CodexACPAgent/data/prompt-v2-turn-error-after-insertion.json b/src/__tests__/CodexACPAgent/data/prompt-v2-turn-error-after-insertion.json index 1af129720..5c0f6d41a 100644 --- a/src/__tests__/CodexACPAgent/data/prompt-v2-turn-error-after-insertion.json +++ b/src/__tests__/CodexACPAgent/data/prompt-v2-turn-error-after-insertion.json @@ -13,6 +13,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { @@ -108,6 +109,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { diff --git a/src/__tests__/CodexACPAgent/data/prompt-v2-turn-failed-or-interrupted.json b/src/__tests__/CodexACPAgent/data/prompt-v2-turn-failed-or-interrupted.json index 57d7982bf..060a6eed3 100644 --- a/src/__tests__/CodexACPAgent/data/prompt-v2-turn-failed-or-interrupted.json +++ b/src/__tests__/CodexACPAgent/data/prompt-v2-turn-failed-or-interrupted.json @@ -13,6 +13,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { @@ -101,6 +102,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { diff --git a/src/__tests__/CodexACPAgent/data/prompt-v2-turn-start-rejected.json b/src/__tests__/CodexACPAgent/data/prompt-v2-turn-start-rejected.json index 285f44ac1..5ac5105ba 100644 --- a/src/__tests__/CodexACPAgent/data/prompt-v2-turn-start-rejected.json +++ b/src/__tests__/CodexACPAgent/data/prompt-v2-turn-start-rejected.json @@ -10,6 +10,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { diff --git a/src/__tests__/CodexACPAgent/data/prompt-v2-typed-failure-after-insertion.json b/src/__tests__/CodexACPAgent/data/prompt-v2-typed-failure-after-insertion.json index a682e58bc..9ef06b549 100644 --- a/src/__tests__/CodexACPAgent/data/prompt-v2-typed-failure-after-insertion.json +++ b/src/__tests__/CodexACPAgent/data/prompt-v2-typed-failure-after-insertion.json @@ -10,6 +10,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { diff --git a/src/__tests__/CodexACPAgent/data/prompt-v2-v1-unchanged.json b/src/__tests__/CodexACPAgent/data/prompt-v2-v1-unchanged.json index cc031b67c..2c1cefa1f 100644 --- a/src/__tests__/CodexACPAgent/data/prompt-v2-v1-unchanged.json +++ b/src/__tests__/CodexACPAgent/data/prompt-v2-v1-unchanged.json @@ -10,6 +10,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { diff --git a/src/__tests__/CodexACPAgent/data/send-attachments-turn-start.json b/src/__tests__/CodexACPAgent/data/send-attachments-turn-start.json index 9d217c0b0..4020bfb24 100644 --- a/src/__tests__/CodexACPAgent/data/send-attachments-turn-start.json +++ b/src/__tests__/CodexACPAgent/data/send-attachments-turn-start.json @@ -33,6 +33,7 @@ "text_elements": [] } ], + "cwd": "cwd", "approvalPolicy": "on-request", "approvalsReviewer": "approvalsReviewer", "sandboxPolicy": { diff --git a/src/__tests__/CodexACPAgent/data/steering-v2-fallback-new-turn.json b/src/__tests__/CodexACPAgent/data/steering-v2-fallback-new-turn.json index 847db402c..0733935a4 100644 --- a/src/__tests__/CodexACPAgent/data/steering-v2-fallback-new-turn.json +++ b/src/__tests__/CodexACPAgent/data/steering-v2-fallback-new-turn.json @@ -10,6 +10,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { diff --git a/src/__tests__/CodexACPAgent/data/steering-v2-injected-owned-turn.json b/src/__tests__/CodexACPAgent/data/steering-v2-injected-owned-turn.json index 144bb13bf..7af8542be 100644 --- a/src/__tests__/CodexACPAgent/data/steering-v2-injected-owned-turn.json +++ b/src/__tests__/CodexACPAgent/data/steering-v2-injected-owned-turn.json @@ -10,6 +10,7 @@ "text_elements": [] } ], + "cwd": "/workspace", "approvalPolicy": "on-request", "approvalsReviewer": "auto_review", "sandboxPolicy": { diff --git a/src/__tests__/CodexACPAgent/prompt-cwd.test.ts b/src/__tests__/CodexACPAgent/prompt-cwd.test.ts new file mode 100644 index 000000000..bf5bcff46 --- /dev/null +++ b/src/__tests__/CodexACPAgent/prompt-cwd.test.ts @@ -0,0 +1,86 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"; +import {connectSession, sessionId, turnStarted, turnCompleted, itemCompleted, userMessageItem, settle} from "./v2-prompt-harness"; +import type {PromptSession} from "./v2-prompt-harness"; + +const prompt = (text = "Hello") => [{type: "text", text}]; + +describe.each([1, 2] as const)("ACP v%s prompt working directory", version => { + let root: string; + let directory: string; + let closeClient: (() => void) | null = null; + + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), "codex-acp-prompt-cwd-")); + directory = fs.mkdtempSync(path.join(root, "project-")); + fs.writeFileSync(path.join(root, "notes.txt"), "not a directory"); + }); + + afterEach(() => { + closeClient?.(); + closeClient = null; + fs.rmSync(root, {recursive: true, force: true}); + }); + + /** Drives the turn the harness just started to completion and answers the prompt. */ + async function completeTurn(client: PromptSession, index: number, id: string) { + const clientUserMessageId = client.turnStartParams[index]!["clientUserMessageId"] as string; + client.emit(turnStarted(id)); + client.emit(itemCompleted(userMessageItem(clientUserMessageId), id)); + client.emit(turnCompleted(id)); + } + + it("forwards _meta.cwd to turn/start and follows it on subsequent turns", async () => { + const client = await connectSession(version); + closeClient = () => client.connection.close(); + + const first = client.request("session/prompt", {sessionId, prompt: prompt(), _meta: {cwd: directory}}); + await vi.waitFor(() => expect(client.turnStartParams).toHaveLength(1)); + expect(client.turnStartParams[0]!["cwd"]).toBe(directory); + await completeTurn(client, 0, "turn-1"); + await expect(first).resolves.toBeDefined(); + await client.promptRunFinished(); + await settle(); + + // The override applies to subsequent turns, so the next prompt without _meta stays there. + const second = client.request("session/prompt", {sessionId, prompt: prompt("Again")}); + await vi.waitFor(() => expect(client.turnStartParams).toHaveLength(2)); + expect(client.turnStartParams[1]!["cwd"]).toBe(directory); + await completeTurn(client, 1, "turn-2"); + await expect(second).resolves.toBeDefined(); + await client.promptRunFinished(1); + await settle(); + }); + + it("leaves the session directory alone when the prompt carries no _meta.cwd", async () => { + const client = await connectSession(version); + closeClient = () => client.connection.close(); + + const response = client.request("session/prompt", {sessionId, prompt: prompt(), _meta: {unrelated: true}}); + await vi.waitFor(() => expect(client.turnStartParams).toHaveLength(1)); + expect(client.turnStartParams[0]!["cwd"]).toBe("/workspace"); + await completeTurn(client, 0, "turn-1"); + await expect(response).resolves.toBeDefined(); + await client.promptRunFinished(); + await settle(); + }); + + it.each([ + ["a relative path", () => "relative"], + ["a nonexistent directory", () => path.join(root, "missing")], + ["a file", () => path.join(root, "notes.txt")], + ["a non-string value", () => 42], + ["an empty string", () => " "], + ])("rejects %s as _meta.cwd before dispatching the prompt", async (_label, value) => { + const client = await connectSession(version); + closeClient = () => client.connection.close(); + + await expect(client.request("session/prompt", {sessionId, prompt: prompt(), _meta: {cwd: value()}})) + .rejects.toMatchObject({code: -32602}); + await client.promptRunFinished(); + await settle(); + expect(client.turnStartParams).toHaveLength(0); + }); +});