github/copilot-sdk · error · InvalidOperationException

LLM inference response StartAsync() called twice.

Error message

LLM inference response StartAsync() called twice.

What it means

LlmInferenceExchange.StartResponseAsync writes the HTTP-style start-of-response (status/headers) exactly once per request. This InvalidOperationException guards against calling the response start twice, which would corrupt the single-response RPC protocol.

Solutions

  1. Guard response-start logic with a local started flag or only call StartAsync once per exchange
  2. Move retry logic so a NEW exchange is used for each attempt instead of reusing one
  3. Ensure error paths use ErrorResponseAsync rather than another StartAsync

Example fix

// before
await exchange.StartAsync(200, "OK", headers);
await exchange.StartAsync(200, "OK", headers); // throws
// after
if (!_responseStarted) { await exchange.StartAsync(200, "OK", headers); _responseStarted = true; }
Defensive patterns

Strategy: type-guard

Validate before calling

if (_started) return; // already started; skip duplicate StartAsync

Type guard

bool CanStart(LlmInferenceExchange ex) => !ex.Started && !ex.Finished;

Try / catch

try { await exchange.StartAsync(200, "OK", headers); }
catch (InvalidOperationException ex) when (ex.Message.Contains("called twice")) { /* ignore duplicate start */ }

Prevention

When it happens

Trigger: Calling StartAsync (or the code path that invokes StartResponseAsync) twice on the same LlmInferenceExchange — e.g. double invocation of a completion callback, or retry logic re-sending the response start on the same exchange.

Common situations: Middleware layered so both a handler and a fallback try to start the response; an error path starts the response and then a catch block starts it again; framework code retries StartAsync after a partial failure.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of github/copilot-sdk@cd8cf15dc3 (2026-09-09). Data as JSON: /api/errors/80a3394e00f8fbca. Report an issue: GitHub.

Appendix: source

Thrown at dotnet/src/CopilotRequestHandler.cs:758

                }

                if (item.Chunk is { Length: > 0 })
                {
                    yield return item.Chunk;
                }
            }
        }
    }

    // --- Response emit (driven by the handler). Strict state machine: ---
    // StartResponseAsync once -> zero or more WriteResponseAsync -> exactly one
    // of EndResponseAsync / ErrorResponseAsync.

    internal async Task StartResponseAsync(int status, string? statusText, IReadOnlyDictionary<string, IReadOnlyList<string>>? headers)
    {
        if (_started)
        {
            throw new InvalidOperationException("LLM inference response StartAsync() called twice.");
        }

        if (_finished)
        {
            throw new InvalidOperationException("LLM inference response already finished.");
        }

        _started = true;
        await ServerRpc()
            .LlmInference.HttpResponseStartAsync(RequestId, status, ToWireHeaders(headers), statusText)
            .ConfigureAwait(false);
    }

    internal Task WriteResponseAsync(ReadOnlyMemory<byte> data) =>
        WriteChunkAsync(Convert.ToBase64String(data.ToArray()), binary: true);

    internal Task WriteResponseAsync(string text)
    {

View on GitHub (pinned to cd8cf15dc3)