Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions apps/server/src/mcp/OrchestratorMcpService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@ import {
import { checkpointWorkspace } from "@t3tools/provider-testing/replayWorkspace";
import * as DateTime from "effect/DateTime";
import * as Deferred from "effect/Deferred";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as Fiber from "effect/Fiber";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Queue from "effect/Queue";
import * as Ref from "effect/Ref";
import * as Result from "effect/Result";
import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";
import * as TestClock from "effect/testing/TestClock";

import { CodexProviderCapabilitiesV2 } from "../orchestration-v2/Adapters/CodexAdapterV2.ts";
import * as CommandReceiptStore from "../orchestration-v2/CommandReceiptStore.ts";
Expand Down Expand Up @@ -1274,6 +1277,7 @@ describe("OrchestratorMcpService provider resolution", () => {
"child run completion",
"child question",
"child approval",
"Node fetch's 300 s idle timeout",
] as const)("a task wait still returns on %s", (wake) =>
Effect.gen(function* () {
const events = yield* Queue.unbounded<OrchestrationV2StoredEvent>();
Expand Down Expand Up @@ -1379,6 +1383,24 @@ describe("OrchestratorMcpService provider resolution", () => {
.delegateTask(scope, { task: "Review the change.", mode: "wait" })
.pipe(Effect.forkChild);
yield* Deferred.await(subscribed);
if (wake === "Node fetch's 300 s idle timeout") {
// Nothing is written during the wait, so undici drops the call at 300 s, before any handle.
const beforeClientTimeout = yield* Fiber.join(waiting).pipe(
Effect.timeoutOption(Duration.seconds(299)),
Effect.forkChild,
);
yield* TestClock.adjust(Duration.seconds(299));
const result = yield* Fiber.join(beforeClientTimeout);
assert.isTrue(
Option.isSome(result),
"a wait without timeoutMs was still silent at 299 s, so a Node MCP client loses the taskId",
);
if (Option.isSome(result)) {
assert.equal(result.value.taskId, taskId);
assert.isTrue(result.value.waitTimedOut);
}
return;
}
ready = true;
if (wake === "parent completion") {
yield* Queue.offer(events, {
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/mcp/OrchestratorMcpService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ import * as PeerForwarding from "../peer/PeerForwarding.ts";
import * as RemoteDelegation from "../peer/RemoteDelegation.ts";

const DEFAULT_WAIT_TIMEOUT_MS = 10 * 60 * 1_000;
// Nothing is written while a task wait blocks, and Node fetch drops a silent call at 300 s.
const DEFAULT_TASK_WAIT_TIMEOUT_MS = 4 * 60 * 1_000;
const MAX_WAIT_TIMEOUT_MS = 60 * 60 * 1_000;
const TASK_WAKE_EVENTS = [
{ thread: "parent", eventType: "subagent.updated" },
Expand Down Expand Up @@ -1698,7 +1700,7 @@ const make = Effect.gen(function* () {
): Effect.fn.Return<OrchestratorMcpDelegateTaskResult, OrchestratorMcpFailure> {
const timeoutMs = Math.min(
MAX_WAIT_TIMEOUT_MS,
Math.max(1, requestedTimeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS),
Math.max(1, requestedTimeoutMs ?? DEFAULT_TASK_WAIT_TIMEOUT_MS),
);
const upgradeWake = threadManagement
.dispatch({
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/provider/agentContextBudget.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"claudeSystemAppendBytes": 5597,
"codexAdditionalContextBytes": 7327,
"mcpToolListJsonBytes": 48131
"mcpToolListJsonBytes": 48130
}
2 changes: 1 addition & 1 deletion packages/contracts/src/orchestratorMcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export const OrchestratorMcpDelegateTaskInput = Schema.Struct({
}),
),
timeoutMs: Schema.optional(Schema.Finite).annotate({
description: "Mode 'wait' only. Default 10 min. A timeout does not cancel the child.",
description: "Mode 'wait' only. Default 4 min. A timeout does not cancel the child.",
}),
clientRequestId: Schema.optional(OrchestratorMcpClientRequestId),
runtimeMode: Schema.optional(
Expand Down
Loading