[Bug] Claude: switching a running thread to Full access doesn't take effect; MCP orchestrator can't see or answer approval requests#22147

opened21d agobyEemiel-os

T3 Code 0.0.46-nightly.20261009.2861, macOS arm64, Claude provider.

1. Full access mid-session has no effect A Claude thread started in Auto or Auto-accept edits is spawned with --permission-mode <mode> but without --allow-dangerously-skip-permissions (confirmed via ps). Switching it to Full access in the composer therefore can't turn off prompts: the SDK refuses to switch into bypassPermissions mid-session (see #1818). The UI shows Full access while approval prompts keep coming.

Expected: either always spawn with --allow-dangerously-skip-permissions (it only allows switching later), or restart the session with the flag when entering Full access, and show the real effective mode.

2. Approval requests are invisible over the orchestrator MCP

  • t3_pending_request_list leaves out approval requests.
  • t3_pending_request_respond can't approve them.
  • pendingRequestCount in thread state reads 0 while an approval is open.

The only signal is an approval_request item in the t3_thread_read activity view. An agent that launched a thread can't tell the thread is blocked, so unattended threads hang for hours. Ours hung for 7.5 hours on a single Write to a sibling worktree under acceptEdits.

Request: add pendingApprovalCount to thread state and a tool that lists approvals, and optionally let an MCP caller answer approvals within its own approved permission mode.

Note

🤖 Claude Opus 5.5 responding on behalf of Theo

Thanks for the report. One part of this is fixed on main in #20037. Please test on the nightly built from main on 2026-10-21, or on the next stable release. One part is still open: t3_pending_request_list and pendingRequestCount still omit approvals; MCP approval responses remain reverted, and switching a running Claude session to Full access is not addressed. If that part still matters to you, please open a new issue for that part only.

Tested current main e07595714a on Linux x86_64 in the real web dev app, Claude Opus 5.5, with isolated state.

  • In Auto-accept edits, Write to an outside-workspace path opened an approval. Selecting Full access alone changed the composer draft; the server thread stayed in Auto-accept edits. Sending a follow-up applied Full access, restarted the session, and the write completed without another approval. The old run showed a provider-stream-closed error during the restart; that interruption is tracked separately in #23082.
  • Auto completed a separate write. Returning to Auto-accept edits and writing a sibling path opened another approval.
  • While that approval was open, real MCP calls returned pendingRequestCount: 1, t3_pending_request_list: {"requestIds":[]}, and an activity item with type: "approval_request", status: "waiting". The zero count did not reproduce. The pending-request tools explicitly cover questions only; approval answering was reverted in 2bc2d80e79. Delegated waits already expose pendingApproval since 6f891541fd.

On the latest nightly, does Full access still prompt after sending the next message, or does t3_thread_read still return a zero count while approval is open? If either persists, please include the exact nightly/Claude versions and the provider log around the mode change (query.close, query.open options and system/init permission mode). The reported macOS arm64 setup was not available here.

Captures (Chromium, 1280×800):

Auto-accept edits approval:

browser-screenshot-localhost-mvj0opje-f2dacb58.png

Full access selected before sending:

browser-screenshot-localhost-mvj0pqu3-2541906c.png

After sending, write completed:

browser-screenshot-localhost-mvj0rlxv-b4eac4e7.png

MCP read-only probe while sibling-path approval was open:

browser-screenshot-localhost-mvj0v36b-91b021da.png

Synced from GitHub

Retested on 0.1.2-nightly.20261021.3499 (Claude Code 2.1.308, macOS arm64).

An Auto-accept edits thread writing to a sibling path outside its workspace opens an approval ("Path is outside allowed working directories"). That's expected, since acceptEdits only auto-approves edits inside the working directory. While it waits:

  • t3_thread_read → pendingRequestCount: 1. The zero count no longer reproduces.
  • t3_pending_request_list → {"requestIds":[]}, as documented.
  • The approval only appears as an approval_request / waiting activity item.

Full access applying on the next send matches what @juliusmarminge described, so I'm treating that part as working as designed.

What's left is narrower than my original report. An orchestrator can tell a launched thread is blocked, the same way t3 thread does (pendingRequestCount > 0 after a timed-out wait slice). But it can't tell an approval from a question, or which request it is, without inferring it from an empty question list. Delegated tasks already get this through pendingApproval (#20037); threads started with t3_thread_launch don't.

Proposed fix

Report the same identity-only pendingApproval: { requestId, kind, runId } | null on t3_thread_read (OrchestratorMcpThreadDetail) and t3_thread_wait (OrchestratorMcpThreadWaitResult). It is the oldest pending live approval of any unfinished run in the thread.

  • No way to answer approvals. Pending request tools stay questions-only, so the #22152 boundary is unchanged.
  • Wait semantics are unchanged. t3_thread_wait still ends only on a terminal run.
  • Backward compatible. Both fields are optional, so older servers stay compatible.
  • Delegated tasks behave the same. pendingTaskApproval is generalized into oldestPendingApproval(projection, ownsRun), with no change to how delegated tasks are reported.
  • Cost. nodes are loaded only when a pending live approval exists. A plain read adds no load, and a wait adds one runtimeRequests read.
  • New visibility. requestId becomes visible to any caller allowed to read the thread. The activity view shows the request but not its id. Delegated tasks already expose this to their parent.

The patch touches 4 files: the server and contracts change, a unit test and a docs note. The test covers:

  • the oldest approval wins;
  • an approval on a finished run is ignored, as are a resolved approval, a question and a message-capability request;
  • no nodes load without an approval;
  • the wait reports the approval, and a quiet wait returns null.

It fails on main and passes with the change. Checks run on top of main 3b95e7831:

  • the MCP tests (225);
  • the contracts tests;
  • server and contracts typecheck;
  • lint and fmt.

I haven't yet tested it end to end against a dev server with a real Claude thread. I'd do that before opening the PR.

Per CONTRIBUTING, I'd rather check first: would you take this as a PR? Happy to adjust the scope. One example is ending t3_thread_wait early on an approval, as delegate_task does in mode=wait, either here or as a follow-up.

Patch (against main 3b95e7831)
diff
1diff --git a/apps/server/src/mcp/OrchestratorMcpService.activity.test.ts b/apps/server/src/mcp/OrchestratorMcpService.activity.test.ts
2index 9d2f10bc6..94af95ab5 100644
3--- a/apps/server/src/mcp/OrchestratorMcpService.activity.test.ts
4+++ b/apps/server/src/mcp/OrchestratorMcpService.activity.test.ts
5@@ -6,7 +6,9 @@ import {
6 ProjectId,
7 ProviderDriverKind,
8 ProviderInstanceId,
9+ ProviderSessionId,
10 RunId,
11+ RuntimeRequestId,
12 ScheduledTaskId,
13 ThreadId,
14 } from "@t3tools/contracts";
15@@ -181,6 +183,143 @@ it("readThread prefers activity-run status over a newer cancelled queued run", a
16 }).pipe(Effect.provide(layer), Effect.runPromise);
17 });
18
19+it("readThread and waitForThread name the oldest live approval of an unfinished run", async () => {
20+ const finishedRunId = RunId.make("run-mcp-finished");
21+ const activeNodeId = NodeId.make("node-mcp-active");
22+ const finishedNodeId = NodeId.make("node-mcp-finished");
23+ const live = { type: "live" as const, providerSessionId: ProviderSessionId.make("session-live") };
24+ const request = (
25+ id: string,
26+ nodeId: NodeId,
27+ kind: string,
28+ createdAt: string,
29+ responseCapability: object = live,
30+ status: "pending" | "resolved" = "pending",
31+ ) => ({
32+ id: RuntimeRequestId.make(id),
33+ nodeId,
34+ providerTurnId: null,
35+ nativeRequestRef: null,
36+ kind,
37+ status,
38+ responseCapability,
39+ createdAt: DateTime.makeUnsafe(createdAt),
40+ resolvedAt: null,
41+ });
42+ const projection = {
43+ thread: baseThread({
44+ threadId: parentThreadId,
45+ title: "Parent",
46+ instanceId: parentInstanceId,
47+ model: "gpt-5.4",
48+ }),
49+ runs: [
50+ makeRun({ id: finishedRunId, ordinal: 1, status: "completed" }),
51+ makeRun({ id: activeRunId, ordinal: 2, status: "running" }),
52+ ],
53+ nodes: [
54+ { id: activeNodeId, runId: activeRunId },
55+ { id: finishedNodeId, runId: finishedRunId },
56+ ],
57+ visibleTurnItems: [],
58+ runtimeRequests: [
59+ // Outlived its finished run's provider callback.
60+ request("request-stale", finishedNodeId, "command", "2026-08-04T11:00:00.000Z"),
61+ request(
62+ "request-answered",
63+ activeNodeId,
64+ "command",
65+ "2026-08-04T11:05:00.000Z",
66+ live,
67+ "resolved",
68+ ),
69+ request("request-question", activeNodeId, "user_input", "2026-08-04T11:10:00.000Z"),
70+ request("request-message", activeNodeId, "command", "2026-08-04T11:20:00.000Z", {
71+ type: "message",
72+ }),
73+ request("request-newer", activeNodeId, "command", "2026-08-04T11:40:00.000Z"),
74+ request("request-oldest", activeNodeId, "file-change", "2026-08-04T11:30:00.000Z"),
75+ ],
76+ messages: [],
77+ contextTransfers: [],
78+ subagents: [],
79+ updatedAt: now,
80+ } as unknown as OrchestrationV2ThreadProjection;
81+ const loadedKinds: Array<ReadonlyArray<string>> = [];
82+
83+ const layer = layerOrchestratorService.pipe(
84+ Layer.provide(
85+ Layer.mergeAll(
86+ Layer.mock(ThreadManagementService.ThreadManagementService)({
87+ getTimelinePage: () => Effect.succeed({ items: [], totalItems: 0, hasMore: false }),
88+ getThreadRecords: (threadId, kinds) => {
89+ loadedKinds.push(kinds);
90+ return threadId === parentThreadId
91+ ? Effect.succeed(projection)
92+ : Effect.die(`unexpected thread ${threadId}`);
93+ },
94+ getThreadShell: (threadId) =>
95+ Effect.succeed(
96+ threadId === parentThreadId
97+ ? (projection.thread as unknown as OrchestrationV2ThreadShell)
98+ : null,
99+ ),
100+ getProjectThreadRecords: (input) =>
101+ input.threadId === parentThreadId
102+ ? Effect.succeed(projection)
103+ : Effect.die(`unexpected thread ${input.threadId}`),
104+ waitForThread: (input) =>
105+ Effect.succeed({
106+ threadId: input.threadId,
107+ run: projection.runs[1]!,
108+ timedOut: true,
109+ }),
110+ } satisfies Partial<ThreadManagementService.ThreadManagementService["Service"]>),
111+ ServerSettings.layerTest(),
112+ Layer.mock(ProviderRegistry.ProviderRegistry)({
113+ getProviders: Effect.succeed([]),
114+ } satisfies Partial<ProviderRegistry.ProviderRegistry["Service"]>),
115+ Layer.mock(ProjectService.ProjectService)({}),
116+ Layer.mock(SecretRequests.SecretRequests)({}),
117+ Layer.mock(ScheduledTaskService.ScheduledTaskService)({
118+ list: () => Effect.succeed({ tasks: [] }),
119+ } satisfies Partial<ScheduledTaskService.ScheduledTaskService["Service"]>),
120+ Layer.mock(ProviderAdapterRegistry.ProviderAdapterRegistryV2)({
121+ list: () => Effect.succeed([]),
122+ } satisfies Partial<ProviderAdapterRegistry.ProviderAdapterRegistryV2["Service"]>),
123+ NodeCrypto.layer,
124+ ),
125+ ),
126+ );
127+
128+ await Effect.gen(function* () {
129+ const service = yield* OrchestratorMcpService.OrchestratorMcpService;
130+ const expected = {
131+ requestId: RuntimeRequestId.make("request-oldest"),
132+ kind: "file-change",
133+ runId: activeRunId,
134+ };
135+ const read = yield* service.readThread(makeScope(), { threadId: parentThreadId });
136+ expect(read.thread.pendingRequestCount).toBe(5);
137+ expect(read.thread.pendingApproval).toEqual(expected);
138+ const waited = yield* service.waitForThread(makeScope(), { threadId: parentThreadId });
139+ expect(waited).toMatchObject({ status: "running", timedOut: true, pendingApproval: expected });
140+ expect(loadedKinds.some((kinds) => kinds.includes("nodes"))).toBe(true);
141+
142+ loadedKinds.length = 0;
143+ const withoutApproval = projection.runtimeRequests.filter(
144+ (candidate) => candidate.kind === "user_input",
145+ );
146+ (projection as { runtimeRequests: unknown }).runtimeRequests = withoutApproval;
147+ const quiet = yield* service.readThread(makeScope(), { threadId: parentThreadId });
148+ expect(quiet.thread.pendingRequestCount).toBe(1);
149+ expect(quiet.thread.pendingApproval).toBeNull();
150+ const quietWait = yield* service.waitForThread(makeScope(), { threadId: parentThreadId });
151+ expect(quietWait.pendingApproval).toBeNull();
152+ expect(loadedKinds.some((kinds) => kinds.includes("nodes"))).toBe(false);
153+ }).pipe(Effect.provide(layer), Effect.runPromise);
154+});
155+
156 it("taskStatus returns task.providerInstanceId rather than the driver kind", async () => {
157 const parentProjection = {
158 thread: baseThread({
159diff --git a/apps/server/src/mcp/OrchestratorMcpService.ts b/apps/server/src/mcp/OrchestratorMcpService.ts
160index edaa8777b..886cf7698 100644
161--- a/apps/server/src/mcp/OrchestratorMcpService.ts
162+++ b/apps/server/src/mcp/OrchestratorMcpService.ts
163@@ -7,6 +7,7 @@ import {
164 NodeId,
165 TurnItemId,
166 type OrchestrationV2Run,
167+ type OrchestrationV2RuntimeRequest,
168 type OrchestrationV2Subagent,
169 type OrchestrationV2ThreadProjection,
170 type OrchestrationV2ThreadShell,
171@@ -22,6 +23,7 @@ import {
172 type OrchestratorMcpDelegateTaskInput,
173 type OrchestratorMcpDelegateTaskResult,
174 type OrchestratorMcpInteractionMode,
175+ type OrchestratorMcpPendingApproval,
176 type OrchestratorMcpDeleteScheduledTaskInput,
177 type OrchestratorMcpRequestSecretInput,
178 type OrchestratorMcpRequestSecretResult,
179@@ -508,34 +510,31 @@ function endsTaskWait(result: OrchestratorMcpDelegateTaskResult): boolean {
180
181 const isApprovalRequestKind = Schema.is(ProviderRequestKind);
182
183-function pendingTaskApproval(
184- child: Pick<OrchestrationV2ThreadProjection, "runs" | "nodes" | "runtimeRequests">,
185- taskId: NodeId,
186- originalRun: OrchestrationV2Run | undefined,
187-) {
188- const taskRunIds = new Set(
189- child.runs
190- .filter(
191- (run) =>
192- !ThreadManagementService.isTerminalRunStatus(run.status) &&
193- (run.delegatedTaskId === taskId ||
194- // Legacy tasks own only their original run, not later turns in the child thread.
195- (run.delegatedTaskId === undefined && run.id === originalRun?.id)),
196- )
197+function isPendingLiveApproval(
198+ request: OrchestrationV2RuntimeRequest,
199+): request is OrchestrationV2RuntimeRequest & { readonly kind: ProviderRequestKind } {
200+ return (
201+ request.status === "pending" &&
202+ request.responseCapability.type === "live" &&
203+ isApprovalRequestKind(request.kind)
204+ );
205+}
206+
207+function oldestPendingApproval(
208+ projection: Pick<OrchestrationV2ThreadProjection, "runs" | "nodes" | "runtimeRequests">,
209+ ownsRun: (run: OrchestrationV2Run) => boolean,
210+): OrchestratorMcpPendingApproval | null {
211+ const runIds = new Set(
212+ projection.runs
213+ .filter((run) => !ThreadManagementService.isTerminalRunStatus(run.status) && ownsRun(run))
214 .map((run) => run.id),
215 );
216- let approval: OrchestratorMcpDelegateTaskResult["pendingApproval"] = null;
217+ let approval: OrchestratorMcpPendingApproval | null = null;
218 let oldestCreatedAt = Infinity;
219- for (const request of child.runtimeRequests) {
220- if (
221- request.status !== "pending" ||
222- request.responseCapability.type !== "live" ||
223- !isApprovalRequestKind(request.kind)
224- ) {
225- continue;
226- }
227- const runId = child.nodes.find((node) => node.id === request.nodeId)?.runId;
228- if (runId === undefined || runId === null || !taskRunIds.has(runId)) continue;
229+ for (const request of projection.runtimeRequests) {
230+ if (!isPendingLiveApproval(request)) continue;
231+ const runId = projection.nodes.find((node) => node.id === request.nodeId)?.runId;
232+ if (runId === undefined || runId === null || !runIds.has(runId)) continue;
233 const createdAt = DateTime.toEpochMillis(request.createdAt);
234 if (createdAt < oldestCreatedAt) {
235 oldestCreatedAt = createdAt;
236@@ -545,6 +544,20 @@ function pendingTaskApproval(
237 return approval;
238 }
239
240+function pendingTaskApproval(
241+ child: Pick<OrchestrationV2ThreadProjection, "runs" | "nodes" | "runtimeRequests">,
242+ taskId: NodeId,
243+ originalRun: OrchestrationV2Run | undefined,
244+) {
245+ return oldestPendingApproval(
246+ child,
247+ (run) =>
248+ run.delegatedTaskId === taskId ||
249+ // Legacy tasks own only their original run, not later turns in the child thread.
250+ (run.delegatedTaskId === undefined && run.id === originalRun?.id),
251+ );
252+}
253+
254 function directAppOwnedChildTasks(
255 parent: Pick<OrchestrationV2ThreadProjection, "thread" | "subagents">,
256 target: Pick<OrchestrationV2ThreadProjection, "thread" | "runs">,
257@@ -889,6 +902,7 @@ function threadDetail(
258 itemCount: number,
259 shell: OrchestrationV2ThreadShell,
260 nowMs: number,
261+ pendingApproval: OrchestratorMcpPendingApproval | null,
262 ): OrchestratorMcpThreadDetail {
263 const latest = ThreadManagementService.latestRun(projection);
264 const active = ThreadManagementService.latestActiveRun(projection);
265@@ -924,6 +938,7 @@ function threadDetail(
266 pendingRequestCount: projection.runtimeRequests.filter(
267 (request) => request.status === "pending",
268 ).length,
269+ pendingApproval,
270 archived: projection.thread.archivedAt !== null,
271 ...threadSettlement(projection.thread),
272 ...threadSnooze(shell, nowMs),
273@@ -1223,6 +1238,22 @@ const make = Effect.gen(function* () {
274 return { ...caller, target } as const;
275 });
276
277+ // Nodes are only loaded when a live approval is pending, to keep plain reads cheap.
278+ const threadPendingApproval = (
279+ threadId: ThreadId,
280+ runtimeRequests: ReadonlyArray<OrchestrationV2RuntimeRequest>,
281+ ) =>
282+ runtimeRequests.some(isPendingLiveApproval)
283+ ? threadManagement
284+ .getThreadRecords(threadId, ["runs", "nodes", "runtimeRequests"], {
285+ omitSettledLeafNodes: true,
286+ })
287+ .pipe(
288+ Effect.map((current) => oldestPendingApproval(current, () => true)),
289+ Effect.mapError(threadManagementFailure),
290+ )
291+ : Effect.succeed(null);
292+
293 /**
294 * A thread caller writes to another thread only while its own run is live,
295 * so a credential that outlived its session cannot reach across threads.
296@@ -2804,7 +2835,13 @@ const make = Effect.gen(function* () {
297 }
298 }
299 return {
300- thread: threadDetail(target, timeline.totalItems, shell, yield* Clock.currentTimeMillis),
301+ thread: threadDetail(
302+ target,
303+ timeline.totalItems,
304+ shell,
305+ yield* Clock.currentTimeMillis,
306+ yield* threadPendingApproval(target.thread.id, target.runtimeRequests),
307+ ),
308 recentRuns: target.runs
309 .toSorted((left, right) => right.ordinal - left.ordinal)
310 .slice(0, input.runLimit ?? DEFAULT_THREAD_RUN_LIMIT)
311@@ -2888,6 +2925,12 @@ const make = Effect.gen(function* () {
312 runId: result.run?.id ?? null,
313 status: result.run === null ? "idle" : runOutcome(result.run),
314 timedOut: result.timedOut,
315+ pendingApproval: yield* threadPendingApproval(
316+ input.threadId,
317+ (yield* threadManagement
318+ .getThreadRecords(input.threadId, ["runtimeRequests"])
319+ .pipe(Effect.mapError(threadManagementFailure))).runtimeRequests,
320+ ),
321 } satisfies OrchestratorMcpThreadWaitResult;
322 }),
323 interruptThread: Effect.fn("OrchestratorMcpService.interruptThread")(function* (scope, input) {
324diff --git a/docs/orchestration-v2/orchestrator-mcp-server.md b/docs/orchestration-v2/orchestrator-mcp-server.md
325index 5f312b61b..34ad9546b 100644
326--- a/docs/orchestration-v2/orchestrator-mcp-server.md
327+++ b/docs/orchestration-v2/orchestrator-mcp-server.md
328@@ -402,6 +402,11 @@ client's `effectiveSnoozed`, so agents and the sidebar agree: a snoozed thread
329 wakes early only when it has a pending request or fails after snoozing. Completed
330 runs, background wakes, and delegated completions keep it snoozed.
331
332+`pendingRequestCount` counts questions and approvals alike. Thread detail also
333+reports `pendingApproval`: the identity of the oldest pending live approval of an
334+unfinished run, or `null`, in the same identity-only shape as delegated tasks. It
335+grants no approval authority, and pending request tools still answer questions only.
336+
337 ### `t3_thread_update`
338
339 Updates metadata for the calling thread or any other thread in the environment.
340@@ -436,7 +441,9 @@ idempotent retries.
341 Waits for a selected run to become `completed`, `failed`, `cancelled`,
342 `interrupted`, or `rolled_back`. Without `runId`, it pins the latest run at call
343 time; an idle thread returns immediately. A timeout reports the latest durable
344-status and does not cancel work.
345+status and does not cancel work. The result also reports `pendingApproval` as in
346+`t3_thread_read`, so a caller can tell a run blocked on an approval from one that
347+is still working. A pending approval does not end the wait.
348
349 ### `t3_thread_interrupt`
350
351diff --git a/packages/contracts/src/orchestratorMcp.ts b/packages/contracts/src/orchestratorMcp.ts
352index c962c3975..49c78b1db 100644
353--- a/packages/contracts/src/orchestratorMcp.ts
354+++ b/packages/contracts/src/orchestratorMcp.ts
355@@ -191,6 +191,14 @@ export const OrchestratorMcpDelegateTaskInput = Schema.Struct({
356 });
357 export type OrchestratorMcpDelegateTaskInput = typeof OrchestratorMcpDelegateTaskInput.Type;
358
359+/** Identity only: holds no request details and grants no approval authority. */
360+export const OrchestratorMcpPendingApproval = Schema.Struct({
361+ requestId: RuntimeRequestId,
362+ kind: ProviderRequestKind,
363+ runId: RunId,
364+});
365+export type OrchestratorMcpPendingApproval = typeof OrchestratorMcpPendingApproval.Type;
366+
367 export const OrchestratorMcpDelegateTaskResult = Schema.Struct({
368 taskId: NodeId,
369 childThreadId: ThreadId,
370@@ -217,9 +225,7 @@ export const OrchestratorMcpDelegateTaskResult = Schema.Struct({
371 description:
372 "Blocking question ends mode=wait. Pass threadId=childThreadId to t3_pending_request_list, t3_pending_request_read (with requestId) and t3_pending_request_respond, or ask the user; the task's completion still wakes this thread.",
373 }),
374- pendingApproval: Schema.NullOr(
375- Schema.Struct({ requestId: RuntimeRequestId, kind: ProviderRequestKind, runId: RunId }),
376- ).annotate({
377+ pendingApproval: Schema.NullOr(OrchestratorMcpPendingApproval).annotate({
378 description:
379 "Identity of the oldest pending live approval for this task's own unfinished work, or null. Ends mode=wait without cancelling the child; completion still wakes this thread. This grants no approval authority. Read the fresh full request in childThreadId before responding: the record can outlive its provider callback. Decisions remain with the child's existing approval holder; pending request tools answer questions only.",
380 }),
381@@ -419,6 +425,13 @@ export const OrchestratorMcpThreadDetail = Schema.Struct({
382 runCount: NonNegativeInt,
383 itemCount: NonNegativeInt,
384 pendingRequestCount: NonNegativeInt,
385+ /** Missing on older servers. */
386+ pendingApproval: Schema.optional(
387+ Schema.NullOr(OrchestratorMcpPendingApproval).annotate({
388+ description:
389+ "Identity of the oldest pending live approval for this thread's unfinished runs, or null. pendingRequestCount also counts it. This grants no approval authority; pending request tools answer questions only, and the record can outlive its provider callback.",
390+ }),
391+ ),
392 archived: Schema.Boolean,
393 settled: Schema.Boolean,
394 settledAt: Schema.NullOr(IsoDateTime),
395@@ -501,6 +514,13 @@ export const OrchestratorMcpThreadWaitResult = Schema.Struct({
396 runId: Schema.NullOr(RunId),
397 status: OrchestratorMcpThreadStatus,
398 timedOut: Schema.Boolean,
399+ /** Missing on older servers. */
400+ pendingApproval: Schema.optional(
401+ Schema.NullOr(OrchestratorMcpPendingApproval).annotate({
402+ description:
403+ "Identity of the oldest pending live approval for this thread's unfinished runs when the wait returned, or null. A run blocked on it keeps running until someone with approval authority answers it. Grants no approval authority.",
404+ }),
405+ ),
406 });
407 export type OrchestratorMcpThreadWaitResult = typeof OrchestratorMcpThreadWaitResult.Type;
408
This conversation has been locked.