fix(mcp): keep wait_for_task_change alive past Claude Code's idle timeout
MCP_TOOL_TIMEOUT (raised by ClaudeDo's launchers) is a wall-clock cap unrelated to Claude Code's idle-silence abort (default 300s for HTTP-transport MCP servers), which no launcher raises. A wait near the 900s recommendation was silently killed at ~300s in any session, launcher or not. WaitForTaskChange now reports MCP progress every 30s while polling, which resets that idle timer. Verified against a real claude -p call (no launcher env overrides) surviving a 341s wait via 60s pings -- the same silence that previously aborted at 300s. Tool description and the list-handler prompt no longer claim ClaudeDo's launchers guarantee a long wait survives.
This commit is contained in:
+27
-6
@@ -1,6 +1,7 @@
|
||||
using System.ComponentModel;
|
||||
using ClaudeDo.Data;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ModelContextProtocol;
|
||||
using ModelContextProtocol.Server;
|
||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||
|
||||
@@ -15,12 +16,22 @@ public sealed class TaskWaitMcpTools
|
||||
// Every ClaudeDo-owned launcher (ClaudeProcess for headless runs, InteractiveLaunchSpecService
|
||||
// for ConPTY sessions) sets MCP_TOOL_TIMEOUT=930000ms on the claude CLI process; this cap
|
||||
// leaves a ~30s margin under that so the tool itself reports TimedOut instead of racing the
|
||||
// client's own abort. A caller running claude with a different MCP_TOOL_TIMEOUT (or none --
|
||||
// the CLI default is 60s) will see its own client-side timeout fire first; this tool has no
|
||||
// way to detect or compensate for that from the server side.
|
||||
// client's own wall-clock abort. A caller running claude with a different MCP_TOOL_TIMEOUT (or
|
||||
// none -- the CLI default is 60s) will see its own client-side timeout fire first; this tool
|
||||
// has no way to detect or compensate for that from the server side.
|
||||
//
|
||||
// MCP_TOOL_TIMEOUT is a separate mechanism from Claude Code's idle-silence abort
|
||||
// (CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT, default 300s for HTTP-transport MCP servers like this
|
||||
// one) -- no ClaudeDo launcher raises that one. Without periodic activity, a wait anywhere
|
||||
// near this cap gets killed at ~300s regardless of launcher. WaitForTaskChange survives that
|
||||
// by reporting MCP progress well under 300s apart (see ProgressReportInterval below), which
|
||||
// resets Claude Code's idle timer.
|
||||
internal const int MaxTimeoutSeconds = 900;
|
||||
private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(500);
|
||||
|
||||
// Not readonly: tests shrink this to observe a progress report without waiting 30s.
|
||||
internal static TimeSpan ProgressReportInterval = TimeSpan.FromSeconds(30);
|
||||
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
|
||||
public TaskWaitMcpTools(IDbContextFactory<ClaudeDoDbContext> dbFactory)
|
||||
@@ -33,9 +44,10 @@ public sealed class TaskWaitMcpTools
|
||||
"polling get_task in a loop. Returns immediately if a task is already outside Queued/Running " +
|
||||
"(an unknown id reports status \"NotFound\" and counts as changed). Pitfall: a planning parent " +
|
||||
"goes Running -> WaitingForChildren while its children are still working, so by default " +
|
||||
"waiting on a parent returns early; see treatWaitingForChildrenAsBusy. Requires the calling " +
|
||||
"claude process to run with MCP_TOOL_TIMEOUT >= 930000 (ms) for a long wait to actually be " +
|
||||
"held open -- ClaudeDo's own launchers already set this.")]
|
||||
"waiting on a parent returns early; see treatWaitingForChildrenAsBusy. Sends MCP progress " +
|
||||
"pings every 30s while waiting so a long wait survives the calling client's own idle-silence " +
|
||||
"abort (Claude Code defaults to killing an MCP call after ~300s of silence) -- this is not " +
|
||||
"guaranteed by every possible MCP client.")]
|
||||
public async Task<WaitForTaskChangeResult> WaitForTaskChange(
|
||||
string[] taskIds,
|
||||
[Description(
|
||||
@@ -47,6 +59,7 @@ public sealed class TaskWaitMcpTools
|
||||
"continues until it reaches WaitingForReview or a terminal status instead of returning " +
|
||||
"as soon as it leaves Running.")]
|
||||
bool treatWaitingForChildrenAsBusy = false,
|
||||
IProgress<ProgressNotificationValue>? progress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (taskIds.Length == 0)
|
||||
@@ -58,12 +71,20 @@ public sealed class TaskWaitMcpTools
|
||||
|
||||
try
|
||||
{
|
||||
var lastProgressAt = DateTime.UtcNow;
|
||||
while (true)
|
||||
{
|
||||
var changed = await CheckOnceAsync(taskIds, treatWaitingForChildrenAsBusy, linked.Token);
|
||||
if (changed.Count > 0)
|
||||
return new WaitForTaskChangeResult(changed, TimedOut: false);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
if (now - lastProgressAt >= ProgressReportInterval)
|
||||
{
|
||||
lastProgressAt = now;
|
||||
progress?.Report(new ProgressNotificationValue { Progress = 0, Message = "Still waiting for a task status change." });
|
||||
}
|
||||
|
||||
await Task.Delay(PollInterval, linked.Token);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user