github / github/copilot-cli

An empty model turn is persisted as `content: null` and permanently bricks the session

Abierto
#4,269 0 comentarios 0 reacciones 0 asignados Ver en GitHub
area:models area:sessions
Lenguaje dominante
Shell
Estrellas
11.2k
Forks
1.9k
Merge medio
14 h 16 min
PR fusionados (30 d)
6

Descripción

## Summary

When a model returns a turn with **no text content and no tool calls**, Copilot persists it and replays it on every subsequent request as an assistant message with `"content": null` and no `tool_calls`. Against a strict OpenAI-compatible endpoint this is rejected, and because the bad message is now part of the conversation history, **every following turn in that session fails identically**. There is no way to recover the session from the UI.

Observed with a local model via Ollama's `/v1/chat/completions`, but the persistence/replay behaviour is provider-independent.

## Error surfaced to the user

```
Execution failed: CAPIError: 400 invalid message content type:
```

## Reproduction

1. Point Copilot at an OpenAI-compatible endpoint that validates message shape (e.g. Ollama `/v1/chat/completions`).
2. Use a model that occasionally ends a turn after reasoning without emitting content or a tool call.
3. Once such a turn occurs, every subsequent message in that session fails with the error above.

The rejected payload shape reduces to:

```bash
curl http://localhost:11434/v1/chat/completions \
-H 'Content-Type: application/json' -d '{
"model": "",
"messages": [
{"role": "user", "content": "hi"},
{"role": "assistant", "content": null}
]}'
# 400 invalid message content type:
```

The same payload **succeeds** if `content` is `""`, or if the field is omitted entirely, or if `tool_calls` is present alongside `content: null`.

## Why this looks like a Copilot-side bug

Per the OpenAI API schema for `ChatCompletionRequestAssistantMessage`:

> The contents of the assistant message. Required unless `tool_calls` or `function_call` is specified.

So `content: null` is only valid when accompanied by `tool_calls`. The server accepting null *with* tool calls and rejecting it *without* is consistent with that rule — the emitted message is the non-conformant part.

## Evidence from the session record

In `~/.copilot/session-state//events.jsonl`, 36 of 37 `assistant.message` events were normal tool-calling turns (`content: ''` plus exactly one `toolRequests` entry). Exactly one had neither:

```
model: | outputTokens: 105
content: ''
toolRequests: []
reasoningText: "<105 tokens of reasoning, then the turn ended>"
```

Every request after that point produced `session.error` with the 400 above. The corresponding rows in `session-store.db` show `assistant_response = NULL` for all subsequent turns in that session.

## Impact

A single empty model response is unrecoverable:

- The session cannot be continued — every new message re-sends the poisoned history.
- There is no UI affordance to delete, edit, or truncate the offending turn.
- The only workaround is abandoning the session and starting a new one, losing the conversation.

This is most likely to affect users running local or smaller models, where empty turns are more common, but the failure mode is not specific to any provider.

## Suggested fixes

Either would resolve it; the second is worth doing regardless:

1. **Serialize empty assistant content as `""` rather than `null`** when there are no tool calls (or omit the message entirely if it carries no content, tool calls, or other payload).
2. **Make an empty turn recoverable** — drop a content-less, tool-call-less assistant message when rebuilding history, and/or let the user delete the last turn so a session can't be permanently bricked by one bad response.

## Environment

- Copilot CLI/SDK: `1.0.73`
- Copilot desktop app on macOS 26.5.2 (Apple Silicon)
- Backend: Ollama 0.32.4, local model via `/v1/chat/completions`

Guía de contribución

Abrir la guía de contribución

Línea de trabajo

Empieza rastreando cómo se serializan y reproducen los eventos assistant.message de ~/.copilot/session-state//events.jsonl en solicitudes de chat completion. Compara el evento vacío con las filas assistant_response de session-store.db y reprodúcelo contra el endpoint /v1/chat/completions de Ollama. Se considera terminado cuando un turno de assistant vacío ya no envenena las solicitudes posteriores y la sesión puede recuperarse sin abandonarla.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
shell
Área
api, backend, cli, databases
Tipo de issue
Error
Dificultad
4/5
Tiempo estimado
3-5 días
Estado de actividad
Tranquilo
Claridad
Bastante claro
Aptitud para principiantes
52/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.