iOfficeAI/OfficeCLI · error · CliException

plugin_idle_timeout

plugin_idle_timeout

Error message

Format-handler plugin '{_plugin.Manifest.Name}' produced no activity for {idleTimeoutSec}s (command={verbForError}).

What it means

Thrown when the plugin produces no activity (no stdout reply and no stderr heartbeat) for idleTimeoutSec seconds during a Send call. The idle watchdog polls stdout at intervals and checks _lastActivityTicks, which the stderr pump resets whenever it sees a {"heartbeat":true} line. This fires when the plugin is stuck, deadlocked, or doing very long work without heartbeating. The plugin process is killed (TryKill) and the session is marked _broken.

Source

Thrown at src/officecli/Core/Plugins/FormatHandlerSession.cs:299

    /// slow to reply.
    /// </summary>
    private string? ReadReplyWithIdleWatchdog(int idleTimeoutSec, string verbForError)
    {
        var budgetTicks = TimeSpan.FromSeconds(idleTimeoutSec).Ticks;
        var readTask = Task.Run(() => _stdoutReader!.ReadLine());

        while (!readTask.IsCompleted)
        {
            // Poll at one-quarter the budget (250ms floor) so even short
            // timeouts fire reasonably close to the configured deadline.
            var pollMs = Math.Max(250, idleTimeoutSec * 1000 / 4);
            if (readTask.Wait(pollMs)) break;
            var since = DateTime.UtcNow.Ticks - Volatile.Read(ref _lastActivityTicks);
            if (since > budgetTicks)
            {
                _broken = true;
                TryKill();
                throw new CliException(
                    $"Format-handler plugin '{_plugin.Manifest.Name}' produced no activity for {idleTimeoutSec}s (command={verbForError}).")
                {
                    Code = "plugin_idle_timeout",
                    Suggestion = $"Raise `idle_timeout_seconds.verbs.{verbForError}` in the plugin's manifest, " +
                                 "emit periodic `{\"heartbeat\":true}` on stderr during long jobs, or pass --timeout 0 to disable.",
                };
            }
        }

        // readTask completed — propagate exceptions, then take the result.
        var line = readTask.GetAwaiter().GetResult();
        if (line is not null)
            Volatile.Write(ref _lastActivityTicks, DateTime.UtcNow.Ticks);
        return line;
    }

    public void Dispose()
    {

View on GitHub (pinned to 1ced45e900)

Solutions

  1. Raise the timeout in the plugin manifest: set idle_timeout_seconds.verbs.<verb> or idle_timeout_seconds.default to a higher value.
  2. Add heartbeat emission to the plugin: periodically write {"heartbeat":true} to stderr during long operations (the stderr pump resets the activity timer).
  3. Disable the watchdog entirely: pass --timeout 0, or set environment variable OFFICECLI_PLUGIN_IDLE_TIMEOUT_SECONDS=0.
  4. Profile the plugin to find the bottleneck — if it genuinely needs minutes, heartbeating is the correct fix, not just raising the limit.
  5. Check for deadlocks: file locks, network hangs, interactive prompts blocking on stdin.

Example fix

// before: plugin does long work with no heartbeat
def handle_save(msg):
    do_heavy_conversion(msg['args'])  # takes 120s, default timeout is 60s
    return ok(result)

// after: emit heartbeats on stderr during long work
def handle_save(msg):
    for chunk in do_heavy_conversion_chunked(msg['args']):
        sys.stderr.write('{"heartbeat":true}\n')
        sys.stderr.flush()
    return ok(result)

// manifest fix: raise the verb-specific budget
"idle_timeout_seconds": { "default": 60, "verbs": { "save": 300 } }
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check the timeout budget for a verb before sending
var budget = plugin.Manifest.ResolveIdleTimeout(command ?? msgType);
if (budget == 0)
{
    // Watchdog disabled — Send will block until the plugin replies or dies.
}
else if (estimatedWorkSeconds > budget)
{
    // Raise the budget or disable: set OFFICECLI_PLUGIN_IDLE_TIMEOUT_SECONDS env var,
    // or update the manifest's idle_timeout_seconds.verbs.<verb>.
    Console.Error.WriteLine($"Warning: estimated {estimatedWorkSeconds}s work exceeds {budget}s budget.");
}

Try / catch

try
{
    var result = session.Send(msgType, command, args, props);
}
catch (CliException ex) when (ex.Code == "plugin_idle_timeout")
{
    // Plugin hung without heartbeating. The process was killed (TryKill).
    // Read ex.Suggestion for remediation guidance.
    session.Dispose();
    // To retry: raise the timeout or add heartbeats, then spawn a new session.
    throw;
}

Prevention

When it happens

Trigger: FormatHandlerSession.SendRaw calls ReadReplyWithIdleWatchdog(idleTimeoutSec, verb). The read task doesn't complete within the budget, and no stderr heartbeat has reset the timer. The budget comes from PluginManifest.ResolveIdleTimeout(verb) — either the manifest's idle_timeout_seconds or the 60s SafeDefault.

Common situations: Plugin does CPU-heavy work (large file rendering, conversion) that takes longer than the 60s default and doesn't emit stderr heartbeats. Plugin deadlocks waiting on a file lock or network resource. Plugin's heartbeat logic has a bug (wrong stream, wrong JSON shape). Plugin waits for user input interactively. Large PowerPoint with hundreds of slides.

Understand the failure class

Related errors


AI-assisted analysis of iOfficeAI/OfficeCLI@1ced45e900 (2026-08-13). Data as JSON: /api/errors/a4316e6097bc22a0. Report an issue: GitHub.