iOfficeAI/OfficeCLI · error · CliException

plugin_stream_closed

plugin_stream_closed

Error message

Format-handler session for '{_plugin.Manifest.Name}' is no longer usable (stream was closed earlier).

What it means

CliException (code 'plugin_stream_closed') thrown by Send when the _broken flag is set. _broken is latched at six sites (FormatHandlerSession.cs:212,234,243,261,268,297) after an earlier IO/protocol failure on the session's stdin/stdout. Once broken, the session refuses further sends rather than retry into a half-dead stream.

Source

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

                ? null
                : JsonSerializer.Deserialize(reply.ToJsonString(), PluginJsonContext.Default.PluginSessionCapabilities);
        }
        catch (JsonException)
        {
            _sessionCaps = null;
        }
    }

    /// <summary>
    /// Send a request envelope and synchronously wait for the matching reply.
    /// Throws <see cref="CliException"/> on protocol error, IO failure, or
    /// plugin-reported error responses.
    /// </summary>
    public JsonNode? Send(string msgType, string? command, JsonObject? args = null, JsonObject? props = null)
    {
        if (_disposed) throw new ObjectDisposedException(nameof(FormatHandlerSession));
        if (_broken)
            throw new CliException(
                $"Format-handler session for '{_plugin.Manifest.Name}' is no longer usable (stream was closed earlier).")
            { Code = "plugin_stream_closed" };

        // Capability gate: short-circuit verbs the plugin already declared it
        // does not support, avoiding a wasted round-trip and ambiguous errors.
        if (command is not null && _sessionCaps?.Capabilities?.Commands is { Count: > 0 } cmds
            && !cmds.Contains(command))
        {
            throw new CliException(
                $"Format-handler plugin '{_plugin.Manifest.Name}' does not implement command '{command}'.")
            { Code = "unsupported_command" };
        }

        var verbForTimeout = command ?? msgType;
        var idle = _plugin.Manifest.ResolveIdleTimeout(verbForTimeout);
        return SendRaw(msgType, command, args, props, idle);
    }

View on GitHub (pinned to 1ced45e900)

Solutions

  1. Recreate the format-handler session (restart the plugin) instead of reusing the broken one.
  2. Investigate the original failure that set _broken — the first error in the log is the root cause, not this one.
  3. For resident hosts, add session-health checks and recycle the proxy on break.
Defensive patterns

Strategy: fallback

Validate before calling

if (session.IsBroken) { session = new FormatHandlerSession(plugin); /* recycle */ }

Type guard

static bool IsSessionUsable(FormatHandlerSession s) => !s.IsBroken && !s.IsDisposed;

Try / catch

try { session.Send(...); }
catch (CliException ex) when (ex.Code == "plugin_stream_closed")
{ session = new FormatHandlerSession(plugin); /* recreate and retry once */ }

Prevention

When it happens

Trigger: Any Send call after a prior request hit an IO error, malformed reply, or stream closure that set _broken=true. ObjectDisposedException is checked first (for explicit disposal).

Common situations: Plugin crashed and the session tried to keep going; a previous command timed out / broke the wire format and subsequent commands reuse the same proxy; long-lived resident session whose plugin was restarted underneath.

Related errors


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