// Reasoning/content boundary tracing — gate-controlled OBSERVABILITY.
//
// WINDSURFAPI_LEAK_TRACE=1 emits structured log lines at the reasoning/content
// boundary so the live-only leak — model reasoning spilling into the content
// channel (PRs #238/#241/#243 rescued empty answers and rerouted a ' thinking'
// prefix, but never reproduced the leak in a forced harness) — can be caught in
// production. Default OFF: when disabled the hot path does nothing beyond one
// env read + boolean compare and nothing else changes.
//
// Every line is prefixed `LEAK_TRACE` and carries structured fields. The three
// call sites:
// - devin-connect-openai.js streamChatWithEmptyRetry: per raw stream event —
// channel (content/reasoning), think markers, truncated sample.
// - handlers/messages.js AnthropicStreamTranslator: block open + per-delta
// classification (blockType/channel).
// - handlers/chat.js streamResponse: settle summary — what went to content vs
// reasoning and whether the thinking→content fallback fired.
import { safeLogValue } from './log-safety.js';
export const LEAK_TRACE_ENV = 'WINDSURFAPI_LEAK_TRACE';
export function leakTraceEnabled(env = process.env) {
return String(env[LEAK_TRACE_ENV] ?? '').trim() === '1';
}
// Marker strings that indicate model reasoning present in a text channel.
// DeepSeek wraps reasoning in …; Kimi K2 emits ◁think▷;
// … is the dialect #250 is named by and the one the #243
// classifier (THINK_OPEN) is built on — the trace must recognize the dialect
// it is deployed to catch. Extend here when another model ships a different
// marker.
export const THINK_MARKERS = ['', '', '', '', '◁think▷'];
export function thinkMarkersIn(text) {
const found = THINK_MARKERS.filter((m) => String(text ?? '').includes(m));
return found.length ? found : null;
}
// Bounded text sample for logs (never the whole payload). Reuses the repo's
// existing log-boundary sanitizer (control chars → '·', slice to max, '…').
export function leakSample(text, max = 120) {
return safeLogValue(text, max);
}