ADR 0256: Preserve DeepSeek reasoning across context compaction
- Status: Accepted
- Date: 2026-09-15
- Deciders: PI-Desktop core
- Amends: D389 / ADR 0136 / ADR 0064
- Fixes: #296
Context
DeepSeek thinking mode requires every replayed Completions assistant message to carry a reasoning field (reasoning_content, or reasoning_text / reasoning on some relays). Official deepseek.com endpoints accept "" for turns that produced no thinking (D389 / #223). OpenCode and third-party relays for DeepSeek V4.1 Flash reject empty echoes with HTTP 400 ("reasoning_text must be passed back").
Codex-shaped compaction (ADR 0064 / ADR 0136) drops assistant turns from the retained tail so tool-call pairs cannot strand and completed user prompts do not look like new tasks. That also drops thinking. Reloading a session rebuilds assistant content from UiMessage.thinking without a Completions thinkingSignature, so convertMessages silently omits the reasoning field and the #223 empty backfill fills "" — which strict relays reject.
Decision
- History rebuild stamps
thinkingSignature: "reasoning_content"only for DeepSeek-compatible OpenAI Completions rows. Other APIs keep unsigned thinking so pi-ai preserves their existing plain-text fallback; their provider-native signatures are not interchangeable with Completions fields. - Checkpoints for models with
requiresReasoningContentOnAssistantMessagesstore the last few thinking turns in opaquedetails.retainedReasoning(text + thinking only, no tool calls). Session context injects those turns between the compaction summary and the user tail only for the live DeepSeek-compatible Completions binding; other providers do not receive synthetic DeepSeek reasoning. - The pi-ai patch backfills the reasoning field already present on the history (
reasoning_content|reasoning_text|reasoning). Official DeepSeek URLs keep empty-string fill. Non-official DeepSeek-family Completions rows setrequiresNonEmptyReasoningReplayand receive the documented placeholder[reasoning not retained for this turn]instead of"". - ADR 0136 retained-tail rules are unchanged: user-only tails, no tool-call replay.
Consequences
- Post-compaction DeepSeek requests on OpenCode / aggregators keep real or placeholder reasoning and no longer 400 on empty echoes.
- Official DeepSeek and #223 empty-backfill behaviour remain available.
- Checkpoint
detailsgrow by a bounded reasoning excerpt; the visible transcript and host schema do not change. - Unit tests cover convertMessages + compaction retained-reasoning wire shape. Live OpenCode / aggregator verification remains deferred (see E2E-005E).
Alternatives
- Degrade to non-thinking mode after the 400: avoids the echo requirement but loses reasoning for that turn and hides the compaction gap.
- Put full assistant/tool messages back in
retainedTail: preserves reasoning but reintroduces stranded tool calls and weakens the task boundary (rejected by ADR 0136).