| author | |
| committer | |
| log | 924fb0c4d4941f9d186e46030ab358ebbdb064f8 |
| tree | ad45a14f30b13acea93af6042e5f158c48b02f22 |
| parent | 755accf590bec6e81d1b2484d8501c95798c1cbe |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: maria-rcks <maria@kuuro.net>9 files changed, 119 insertions(+), 14 deletions(-)
apps/server/src/mcp/OrchestratorMcpService.test.ts+9| ... | ... | @@ -96,6 +96,7 @@ describe("OrchestratorMcpService", () => { |
| 96 | 96 | messages: [], |
| 97 | 97 | subagents: [], |
| 98 | 98 | providerThreads: [], |
| 99 | runtimeRequests: [], | |
| 99 | 100 | } as unknown as OrchestrationV2ThreadProjection; |
| 100 | 101 | let hasNestedWork = true; |
| 101 | 102 | const layerDependencies = Layer.mergeAll( |
| ... | ... | @@ -223,6 +224,7 @@ describe("OrchestratorMcpService", () => { |
| 223 | 224 | messages: [], |
| 224 | 225 | subagents: [], |
| 225 | 226 | providerThreads: [], |
| 227 | runtimeRequests: [], | |
| 226 | 228 | turnItems: [], |
| 227 | 229 | } as unknown as OrchestrationV2ThreadProjection; |
| 228 | 230 | const layerDependencies = Layer.mergeAll( |
| ... | ... | @@ -308,6 +310,7 @@ describe("OrchestratorMcpService", () => { |
| 308 | 310 | messages: [], |
| 309 | 311 | subagents: [], |
| 310 | 312 | providerThreads: [], |
| 313 | runtimeRequests: [], | |
| 311 | 314 | } as unknown as OrchestrationV2ThreadProjection; |
| 312 | 315 | const layerDependencies = Layer.mergeAll( |
| 313 | 316 | NodeServices.layer, |
| ... | ... | @@ -385,6 +388,7 @@ describe("OrchestratorMcpService", () => { |
| 385 | 388 | messages: [], |
| 386 | 389 | subagents: [], |
| 387 | 390 | providerThreads: [], |
| 391 | runtimeRequests: [], | |
| 388 | 392 | } as unknown as OrchestrationV2ThreadProjection; |
| 389 | 393 | const layerDependencies = Layer.mergeAll( |
| 390 | 394 | NodeServices.layer, |
| ... | ... | @@ -465,6 +469,7 @@ describe("OrchestratorMcpService", () => { |
| 465 | 469 | messages: [], |
| 466 | 470 | subagents: [], |
| 467 | 471 | providerThreads: [], |
| 472 | runtimeRequests: [], | |
| 468 | 473 | } as unknown as OrchestrationV2ThreadProjection; |
| 469 | 474 | const layerDependencies = Layer.mergeAll( |
| 470 | 475 | NodeServices.layer, |
| ... | ... | @@ -552,6 +557,7 @@ describe("OrchestratorMcpService", () => { |
| 552 | 557 | messages: [], |
| 553 | 558 | subagents: [], |
| 554 | 559 | providerThreads: [], |
| 560 | runtimeRequests: [], | |
| 555 | 561 | } as unknown as OrchestrationV2ThreadProjection; |
| 556 | 562 | const layerDependencies = Layer.mergeAll( |
| 557 | 563 | NodeServices.layer, |
| ... | ... | @@ -655,6 +661,7 @@ describe("OrchestratorMcpService", () => { |
| 655 | 661 | appOwnedTask("node:mcp-cancel-grandchild-below", childThreadId, grandchildThreadId), |
| 656 | 662 | ], |
| 657 | 663 | providerThreads: [], |
| 664 | runtimeRequests: [], | |
| 658 | 665 | }, |
| 659 | 666 | ], |
| 660 | 667 | [ |
| ... | ... | @@ -666,6 +673,7 @@ describe("OrchestratorMcpService", () => { |
| 666 | 673 | messages: [], |
| 667 | 674 | subagents: [], |
| 668 | 675 | providerThreads: [], |
| 676 | runtimeRequests: [], | |
| 669 | 677 | }, |
| 670 | 678 | ], |
| 671 | 679 | ]) as unknown as ReadonlyMap<ThreadId, OrchestrationV2ThreadProjection>; |
| ... | ... | @@ -883,6 +891,7 @@ describe("OrchestratorMcpService provider resolution", () => { |
| 883 | 891 | messages: [], |
| 884 | 892 | subagents: [], |
| 885 | 893 | providerThreads: [], |
| 894 | runtimeRequests: [], | |
| 886 | 895 | turnItems: [], |
| 887 | 896 | } as unknown as OrchestrationV2ThreadProjection; |
| 888 | 897 |
apps/server/src/mcp/OrchestratorMcpService.ts+30-12| ... | ... | @@ -101,13 +101,14 @@ import * as SecretRequests from "../secrets/SecretRequests.ts"; |
| 101 | 101 | |
| 102 | 102 | const DEFAULT_WAIT_TIMEOUT_MS = 10 * 60 * 1_000; |
| 103 | 103 | const MAX_WAIT_TIMEOUT_MS = 60 * 60 * 1_000; |
| 104 | // Events that can make a delegated task terminal: the parent's task record, | |
| 105 | // and the child's runs, nested tasks, and pending provider background work. | |
| 104 | // Events that can end a delegated task wait: the parent's task record, | |
| 105 | // and the child's runs, nested tasks, provider background work, and questions. | |
| 106 | 106 | const TASK_WAKE_EVENTS = [ |
| 107 | 107 | { thread: "parent", eventType: "subagent.updated" }, |
| 108 | 108 | { thread: "child", eventType: "run.updated" }, |
| 109 | 109 | { thread: "child", eventType: "subagent.updated" }, |
| 110 | 110 | { thread: "child", eventType: "provider-thread.updated" }, |
| 111 | { thread: "child", eventType: "runtime-request.updated" }, | |
| 111 | 112 | ] as const; |
| 112 | 113 | /** A person answers the card, so a slower poll is plenty. */ |
| 113 | 114 | const SECRET_REQUEST_POLL_INTERVAL_MS = 500; |
| ... | ... | @@ -1351,7 +1352,14 @@ const make = Effect.gen(function* () { |
| 1351 | 1352 | const childControls = yield* threadManagement |
| 1352 | 1353 | .getThreadRecords( |
| 1353 | 1354 | task.childThreadId, |
| 1354 | ["runs", "messages", "contextTransfers", "subagents", "providerThreads"], | |
| 1355 | [ | |
| 1356 | "runs", | |
| 1357 | "messages", | |
| 1358 | "contextTransfers", | |
| 1359 | "subagents", | |
| 1360 | "providerThreads", | |
| 1361 | "runtimeRequests", | |
| 1362 | ], | |
| 1355 | 1363 | { messageRoles: ["user"] }, |
| 1356 | 1364 | ) |
| 1357 | 1365 | .pipe(Effect.mapError(threadManagementFailure)); |
| ... | ... | @@ -1456,6 +1464,15 @@ const make = Effect.gen(function* () { |
| 1456 | 1464 | : subagentResultForRun(childProjection, terminalRun).text |
| 1457 | 1465 | : null, |
| 1458 | 1466 | latestTerminalResultContextTransferId: resultTransferForRun(terminalRun)?.id ?? null, |
| 1467 | // A finished task has no question of its own; later child runs belong to other tasks. | |
| 1468 | hasPendingQuestion: | |
| 1469 | !isTerminalTaskStatus(status) && | |
| 1470 | childControls.runtimeRequests.some( | |
| 1471 | (request) => | |
| 1472 | request.kind === "user_input" && | |
| 1473 | request.status === "pending" && | |
| 1474 | request.responseCapability.type === "live", | |
| 1475 | ), | |
| 1459 | 1476 | waitTimedOut, |
| 1460 | 1477 | } satisfies OrchestratorMcpDelegateTaskResult; |
| 1461 | 1478 | if ( |
| ... | ... | @@ -1508,7 +1525,9 @@ const make = Effect.gen(function* () { |
| 1508 | 1525 | .getThreadEventSequence(scope.thread.threadId) |
| 1509 | 1526 | .pipe(Effect.mapError(streamError)); |
| 1510 | 1527 | const initial = yield* readTask(scope, taskId, false, true); |
| 1511 | if (isTerminalTaskStatus(initial.status)) return Option.some(initial); | |
| 1528 | if (initial.hasPendingQuestion || isTerminalTaskStatus(initial.status)) { | |
| 1529 | return Option.some(initial); | |
| 1530 | } | |
| 1512 | 1531 | // One stream per event type, so transcript events never fill a buffer. |
| 1513 | 1532 | return yield* Stream.mergeAll( |
| 1514 | 1533 | TASK_WAKE_EVENTS.map(({ thread, eventType }) => |
| ... | ... | @@ -1522,7 +1541,7 @@ const make = Effect.gen(function* () { |
| 1522 | 1541 | ).pipe( |
| 1523 | 1542 | Stream.mapError(streamError), |
| 1524 | 1543 | Stream.mapEffect(() => readTask(scope, taskId, false, true)), |
| 1525 | Stream.filter((result) => isTerminalTaskStatus(result.status)), | |
| 1544 | Stream.filter((result) => result.hasPendingQuestion || isTerminalTaskStatus(result.status)), | |
| 1526 | 1545 | Stream.runHead, |
| 1527 | 1546 | ); |
| 1528 | 1547 | }).pipe(Effect.timeoutOption(Duration.millis(timeoutMs)), Effect.map(Option.flatten)); |
| ... | ... | @@ -2083,13 +2102,12 @@ const make = Effect.gen(function* () { |
| 2083 | 2102 | Math.max(1, input.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS), |
| 2084 | 2103 | ); |
| 2085 | 2104 | const waited = yield* waitForTask(scope, taskId, timeoutMs); |
| 2086 | if (Option.isSome(waited)) { | |
| 2105 | if (Option.isSome(waited) && isTerminalTaskStatus(waited.value.status)) { | |
| 2087 | 2106 | return waited.value; |
| 2088 | 2107 | } |
| 2089 | // The blocking wait timed out, so it no longer owns delivery: upgrade | |
| 2090 | // the task so a later terminal wakes the parent even mid-turn. Best | |
| 2091 | // effort; on failure the settled_only policy still wakes a settled | |
| 2092 | // parent. | |
| 2108 | // An incomplete wait gives up delivery: upgrade so a later terminal | |
| 2109 | // wakes the parent even mid-turn. Best effort; settled_only still | |
| 2110 | // wakes a settled parent if the upgrade fails. | |
| 2093 | 2111 | yield* threadManagement |
| 2094 | 2112 | .dispatch({ |
| 2095 | 2113 | type: "delegated_task.wake-policy", |
| ... | ... | @@ -2103,7 +2121,7 @@ const make = Effect.gen(function* () { |
| 2103 | 2121 | completionWake: "always", |
| 2104 | 2122 | }) |
| 2105 | 2123 | .pipe( |
| 2106 | // The tool result is the timed-out task either way, so failures | |
| 2124 | // The tool result still returns after a timeout or question, so failures | |
| 2107 | 2125 | // stay warnings. Keep the two shapes apart: a rejected receipt |
| 2108 | 2126 | // means this exact command id already failed (a replay of a |
| 2109 | 2127 | // no-op upgrade), while anything else is a fresh dispatch fault. |
| ... | ... | @@ -2125,7 +2143,7 @@ const make = Effect.gen(function* () { |
| 2125 | 2143 | }), |
| 2126 | 2144 | ), |
| 2127 | 2145 | ); |
| 2128 | return yield* readTask(scope, taskId, true, true); | |
| 2146 | return yield* readTask(scope, taskId, Option.isNone(waited), true); | |
| 2129 | 2147 | }), |
| 2130 | 2148 | taskStatus: (callerScope, taskId) => |
| 2131 | 2149 | Effect.gen(function* () { |
apps/server/src/mcp/OrchestratorMcpToolkit.integration.test.ts+61| ... | ... | @@ -30,6 +30,7 @@ import { |
| 30 | 30 | ProviderThreadId, |
| 31 | 31 | ProviderTurnId, |
| 32 | 32 | type ScheduledTask, |
| 33 | RuntimeRequestId, | |
| 33 | 34 | ScheduledTaskId, |
| 34 | 35 | type ScheduledTaskUpsertInput, |
| 35 | 36 | type ServerProvider, |
| ... | ... | @@ -2722,6 +2723,66 @@ describe("orchestrator MCP toolkit", () => { |
| 2722 | 2723 | ).toMatchObject({ state: "disposed" }); |
| 2723 | 2724 | yield* expectOffersToStay(0); |
| 2724 | 2725 | |
| 2726 | // A live question ends the wait and leaves completion delivery armed. | |
| 2727 | const questionWait = yield* invoke("delegate_task", { | |
| 2728 | task: cancellationPrompt, | |
| 2729 | title: "Wait question", | |
| 2730 | target: { providerInstanceId: codexInstanceId, model: codexModel }, | |
| 2731 | mode: "wait", | |
| 2732 | timeoutMs: 30_000, | |
| 2733 | clientRequestId: "delegate-wait-question-1", | |
| 2734 | }).pipe(Effect.forkChild); | |
| 2735 | const question = (yield* waitForProjection(orchestrator, parentThreadId, (p) => | |
| 2736 | p.subagents.some( | |
| 2737 | (task) => task.title === "Wait question" && task.childThreadId !== null, | |
| 2738 | ), | |
| 2739 | )).subagents.find((task) => task.title === "Wait question")!; | |
| 2740 | const asking = yield* waitForProjection(orchestrator, question.childThreadId!, (p) => | |
| 2741 | p.providerTurns.some((turn) => turn.status === "running"), | |
| 2742 | ); | |
| 2743 | const askedAt = yield* DateTime.now; | |
| 2744 | yield* (yield* EventSink.EventSinkV2).write({ | |
| 2745 | events: [ | |
| 2746 | { | |
| 2747 | id: EventId.make("event:wait-question:live"), | |
| 2748 | type: "runtime-request.updated", | |
| 2749 | threadId: question.childThreadId!, | |
| 2750 | occurredAt: askedAt, | |
| 2751 | payload: { | |
| 2752 | id: RuntimeRequestId.make("request:wait-question"), | |
| 2753 | nodeId: question.id, | |
| 2754 | providerTurnId: null, | |
| 2755 | nativeRequestRef: null, | |
| 2756 | kind: "user_input", | |
| 2757 | status: "pending", | |
| 2758 | responseCapability: { | |
| 2759 | type: "live", | |
| 2760 | providerSessionId: asking.providerSessions[0]!.id, | |
| 2761 | }, | |
| 2762 | createdAt: askedAt, | |
| 2763 | resolvedAt: null, | |
| 2764 | }, | |
| 2765 | }, | |
| 2766 | ], | |
| 2767 | }); | |
| 2768 | expect((yield* Fiber.join(questionWait)).structuredContent).toMatchObject({ | |
| 2769 | status: "running", | |
| 2770 | hasPendingQuestion: true, | |
| 2771 | waitTimedOut: false, | |
| 2772 | }); | |
| 2773 | expect( | |
| 2774 | (yield* orchestrator.getThreadProjection(parentThreadId)).subagents.find( | |
| 2775 | (task) => task.id === question.id, | |
| 2776 | )?.completionWake, | |
| 2777 | ).toBe("always"); | |
| 2778 | yield* invoke("task_cancel", { | |
| 2779 | taskId: question.id, | |
| 2780 | clientRequestId: "cancel-wait-question-1", | |
| 2781 | }); | |
| 2782 | yield* waitForProjection(orchestrator, parentThreadId, (p) => | |
| 2783 | p.subagents.some((task) => task.id === question.id && task.status === "interrupted"), | |
| 2784 | ); | |
| 2785 | ||
| 2725 | 2786 | // The MCP tool cannot force the reverse interleaving (child |
| 2726 | 2787 | // terminal before the upgrade lands), so dispatch the command |
| 2727 | 2788 | // directly. The first wait-mode delegation completed while the |
apps/server/src/mcp/toolkits/orchestrator/tools.ts+1-1| ... | ... | @@ -65,7 +65,7 @@ const OrchestratorCapabilitiesTool = Tool.make("orchestrator_capabilities", { |
| 65 | 65 | |
| 66 | 66 | export const DelegateTaskTool = Tool.make("delegate_task", { |
| 67 | 67 | description: |
| 68 | "Needs an agent running inside a T3 thread. Delegate one task to a T3-owned child agent/subagent of THIS thread and run it with only the supplied task prompt, without copying parent conversation history. Choose providers and models from orchestrator_capabilities, which uses the same live catalog as the composer. Prefer native subagent tools for same-provider work only when they support the chosen model. Use this for any model missing from the native tool, including same-provider work, for cross-provider work, or for explicitly T3-owned child tasks. For every T3 delegated review round, call delegate_task again with the original brief, prior findings, responses, and unresolved objections in the task prompt. Track each round by its own taskId and use a distinct clientRequestId per round, stable across retries of that round. The childThreadId is backing storage, not the target for starting another delegated review round through t3_thread_send. Set isolation to worktree for a separate checkout based on the parent local commit, with project setup completed before the child starts. Uncommitted edits are not copied; omitted isolation shares the parent checkout. Provider, model, model options (see orchestrator_capabilities), runtime mode, and interaction mode inherit unless target overrides them. A requested runtimeMode must be one the target provider lists in orchestrator_capabilities runtimeModes; an inherited one steps down to the broadest mode it offers. Leave runtimeMode unset unless the user asks for a supervised child: approval-required and auto-accept-edits usually make the child wait for a person at each action that needs approval. Prefer mode='async' for long work; mode='wait' blocks until completion or timeout. timeoutMs on mode=wait is only the parent's wait budget and does not cancel the child. waitTimedOut on that wait call means the timeout fired; keep that taskId and read status on later task_status. An async child's completion wakes this thread through a notification, steered into active turns where supported or queued otherwise, so end the turn instead of polling or spawning watchers; use task_status only when the result is needed mid-turn.", | |
| 68 | "Needs an agent running inside a T3 thread. Delegate one task to a T3-owned child agent/subagent of THIS thread and run it with only the supplied task prompt, without copying parent conversation history. Choose providers and models from orchestrator_capabilities, which uses the same live catalog as the composer. Prefer native subagent tools for same-provider work only when they support the chosen model. Use this for any model missing from the native tool, including same-provider work, for cross-provider work, or for explicitly T3-owned child tasks. For every T3 delegated review round, call delegate_task again with the original brief, prior findings, responses, and unresolved objections in the task prompt. Track each round by its own taskId and use a distinct clientRequestId per round, stable across retries of that round. The childThreadId is backing storage, not the target for starting another delegated review round through t3_thread_send. Set isolation to worktree for a separate checkout based on the parent local commit, with project setup completed before the child starts. Uncommitted edits are not copied; omitted isolation shares the parent checkout. Provider, model, model options (see orchestrator_capabilities), runtime mode, and interaction mode inherit unless target overrides them. A requested runtimeMode must be one the target provider lists in orchestrator_capabilities runtimeModes; an inherited one steps down to the broadest mode it offers. Leave runtimeMode unset unless the user asks for a supervised child: approval-required and auto-accept-edits usually make the child wait for a person at each action that needs approval. Prefer mode='async' for long work; mode='wait' blocks until completion, a blocking question, or timeout. If hasPendingQuestion, 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. timeoutMs on mode=wait is only the parent's wait budget and does not cancel the child. waitTimedOut on that wait call means the timeout fired; keep that taskId and read status on later task_status. An async child's completion wakes this thread through a notification, steered into active turns where supported or queued otherwise, so end the turn instead of polling or spawning watchers; use task_status only when the result is needed mid-turn.", | |
| 69 | 69 | parameters: OrchestratorMcpDelegateTaskInput, |
| 70 | 70 | success: OrchestratorMcpDelegateTaskResult, |
| 71 | 71 | failure: OrchestratorMcpFailure, |
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.test.ts+1| ... | ... | @@ -670,6 +670,7 @@ describe("CodexAdapterV2 process spawning", () => { |
| 670 | 670 | mcp_servers: { |
| 671 | 671 | "t3-code": { |
| 672 | 672 | url: "http://127.0.0.1:43123/mcp", |
| 673 | tool_timeout_sec: 3_900, | |
| 673 | 674 | http_headers: { |
| 674 | 675 | Authorization: "Bearer secret-codex-token", |
| 675 | 676 | }, |
apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts+5| ... | ... | @@ -1324,6 +1324,10 @@ export const CODEX_THREAD_CONFIG = { |
| 1324 | 1324 | mcp_optional_startup_grace_ms: 10_000, |
| 1325 | 1325 | } as const; |
| 1326 | 1326 | |
| 1327 | // T3 wait tools can block for up to an hour. Keep Codex's MCP timeout above | |
| 1328 | // that limit so the server's own wait timeout ends a long call. | |
| 1329 | const CODEX_T3_MCP_TOOL_TIMEOUT_SEC = 65 * 60; | |
| 1330 | ||
| 1327 | 1331 | export function codexThreadRuntimeParams(input: { |
| 1328 | 1332 | readonly mcpSession: McpProviderSession.McpProviderSessionConfig | undefined; |
| 1329 | 1333 | readonly modelSelection?: { readonly model: string }; |
| ... | ... | @@ -1345,6 +1349,7 @@ export function codexThreadRuntimeParams(input: { |
| 1345 | 1349 | mcp_servers: { |
| 1346 | 1350 | "t3-code": { |
| 1347 | 1351 | url: mcpSession.endpoint, |
| 1352 | tool_timeout_sec: CODEX_T3_MCP_TOOL_TIMEOUT_SEC, | |
| 1348 | 1353 | http_headers: { |
| 1349 | 1354 | Authorization: mcpSession.authorizationHeader, |
| 1350 | 1355 | }, |
docs/orchestration-v2/orchestrator-mcp-server.md+7-1| ... | ... | @@ -259,7 +259,12 @@ provider session. The request becomes the V2 command |
| 259 | 259 | `mode: "async"` returns the current durable state immediately. |
| 260 | 260 | `mode: "wait"` waits for the task result, including nested work and completion follow-ups, or until |
| 261 | 261 | the timeout expires. A wait timeout does not cancel the child; the result sets |
| 262 | `waitTimedOut: true`, and the caller can continue with `task_status`. | |
| 262 | `waitTimedOut: true`, and the caller can continue with `task_status`. A pending blocking | |
| 263 | question also ends the wait with `hasPendingQuestion: true` and `waitTimedOut: false`. | |
| 264 | If `hasPendingQuestion`, pass `threadId=childThreadId` to `t3_pending_request_list`, | |
| 265 | `t3_pending_request_read` (with `requestId`) and `t3_pending_request_respond`, or ask | |
| 266 | the user; the task's completion still wakes this thread. Async questions and approvals | |
| 267 | do not end the wait. | |
| 263 | 268 | |
| 264 | 269 | ```ts |
| 265 | 270 | type DelegateTaskResult = { |
| ... | ... | @@ -278,6 +283,7 @@ type DelegateTaskResult = { |
| 278 | 283 | latestTerminalStatus: "completed" | "failed" | "cancelled" | "interrupted" | null; |
| 279 | 284 | latestTerminalSummary: string | null; |
| 280 | 285 | latestTerminalResultContextTransferId: string | null; |
| 286 | hasPendingQuestion: boolean; | |
| 281 | 287 | waitTimedOut: boolean; |
| 282 | 288 | }; |
| 283 | 289 | ``` |
packages/contracts/src/orchestratorMcp.test.ts+1| ... | ... | @@ -58,6 +58,7 @@ describe("orchestrator MCP contracts", () => { |
| 58 | 58 | latestTerminalStatus: "completed", |
| 59 | 59 | latestTerminalSummary: "Workspace inspected.", |
| 60 | 60 | latestTerminalResultContextTransferId: "context-transfer-result-1", |
| 61 | hasPendingQuestion: false, | |
| 61 | 62 | waitTimedOut: false, |
| 62 | 63 | }); |
| 63 | 64 |
packages/contracts/src/orchestratorMcp.ts+4| ... | ... | @@ -217,6 +217,10 @@ export const OrchestratorMcpDelegateTaskResult = Schema.Struct({ |
| 217 | 217 | model: Schema.NullOr(Schema.String), |
| 218 | 218 | summary: Schema.NullOr(Schema.String), |
| 219 | 219 | resultContextTransferId: Schema.NullOr(ContextTransferId), |
| 220 | hasPendingQuestion: Schema.Boolean.annotate({ | |
| 221 | description: | |
| 222 | "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.", | |
| 223 | }), | |
| 220 | 224 | waitTimedOut: Schema.Boolean.annotate({ |
| 221 | 225 | description: |
| 222 | 226 | "True only on that mode=wait call when timeoutMs elapsed. The timeout does not cancel the child. Later task_status reads return false and use status for liveness.", |