Skip to content

feat(trace): WINDSURFAPI_LEAK_TRACE — structured reasoning/content boundary logs for catching live reasoning leaks (opt-in, default OFF) - #249

Open
warelik wants to merge 2 commits into
dwgx:masterfrom
warelik:pr/reasoning-leak-trace
Open

feat(trace): WINDSURFAPI_LEAK_TRACE — structured reasoning/content boundary logs for catching live reasoning leaks (opt-in, default OFF)#249
warelik wants to merge 2 commits into
dwgx:masterfrom
warelik:pr/reasoning-leak-trace

Conversation

@warelik

@warelik warelik commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

中文 TL;DR

已知问题(见文末 issue):thinking 模型偶尔把 reasoning 写进 CONTENT 通道;只能在真实会话中偶发复现,受控探针复现不出来,所以下一层修复一直缺现场数据。本 PR 在 reasoning/content 边界加一套结构化日志:devin-connect-openai 事件层、messages 分类层、chat 出口层,记录通道、think-标记状态、有限文本采样、reqId/acct。由 WINDSURFAPI_LEAK_TRACE 门控,默认关闭;关闭时行为逐字不变、热路径零额外开销。本 PR 只做可观测性,不做修复;抓到现场后另开 PR 提修复。

Зачем

Протекание мыслей в контент — известная проблема, воспроизводится только живьём. Каждое поколение защиты строилось по полевой сигнатуре, а не по репродукции: #238 (rescue пустых ответов), #241 (точность rescue), #243 (перекладка ведущего <think>-блока из content в thinking). Чтобы предложить фикс следующего слоя, нужен пойманный инцидент: какой канал, какие маркеры, какой префикс. Этот PR даёт удочку.

Точки лога

  • src/devin-connect-openai.js — сырые события границы: канал (content/reasoning), наличие think-маркеров, ограниченный сэмпл текста, reqId/acct.
  • src/handlers/messages.js — решения классификации/перекладки блоков.
  • src/handlers/chat.js — что ушло в content на settle/finish, соотношение размеров reasoning и content.

Модуль src/leak-trace.js: гейт, маркеры (включая ◁think▷ как известный диалект Kimi K2), обрезка сэмплов через существующий log-safety — в лог не пишутся ключи и полные ответы.

Safety

  • Гейт WINDSURFAPI_LEAK_TRACE, default OFF. При выключенном гейте поведение побайтово неизменно, горячий путь не делает лишней работы кроме чтения флажка.
  • Документация в .env.example и README.

Test plan

  • test/leak-trace.test.js: при включённом гейте — лог-строки с ожидаемыми полями на всех трёх точках (подмена логгера по прецеденту test/retry-rescue-budget-split.test.js); при выключенном гейте на том же горячем пути — ни одной строки.
  • Мутационная spec test/mutations/leak-trace.json (6 мутаций, все CAUGHT) + все spec репо: EXIT=0, anchor'ы ровно по разу.
  • Полный сьют: 3607 pass / 0 fail.

W ARELIK and others added 2 commits August 6, 2026 18:10
… boundary logs (default OFF)

Gate WINDSURFAPI_LEAK_TRACE=1 enables LEAK_TRACE-prefixed structured log
lines at the reasoning/content boundary to catch the live-only reasoning
@dwgx

dwgx commented Aug 7, 2026

Copy link
Copy Markdown
Owner

评审:门控和脱敏我逐条驱动过,都成立。请补一条 —— 标记表认不出 #250 用来命名这条泄漏的那个方言。

我实跑过什么

head dd0f95e,worktree 隔离:

结果
npm run test:release 3607 pass / 0 fail(275 个文件) —— 与你声明的一致
leak-trace.json 6/6 如声明
dependencies 仍为空

「门关时热路径只多一次 env 读 + 布尔比较」这句我特意攻过,它成立。 我原本怀疑的是调用点参数求值 —— trace(buildSample(bigText)) 这种写法,callee 立刻返回也照付构造成本。你四个调用点全部是 if (leakTraceEnabled()) { … } 先行,采样构造在条件内部,所以那条不成立。leakSample 复用 safeLogValue 而不是自己写截断,这一处也对 —— 脱敏边界只有一份实现。

三个埋点的分层选得准:事件层拿原始通道、分类层拿决策、出口层拿 settle 后的尺寸比。要定位「是哪一层把 reasoning 放进 content 的」,这三个位置是最小充分集。


M1(请补)— <think> 不在标记表里,而 #250 正是以它命名的

leak-trace.js:29:

export const THINK_MARKERS = ['<thinking>', '</thinking>', '◁think▷'];

我驱动 thinkMarkersIn:

"<think>reasoning</think>"   -> null          ← 认不出
"<thinking>x</thinking>"     -> ["<thinking>","</thinking>"]
"◁think▷y"                   -> ["◁think▷"]

#250 的正文写的是:

thinking 模型偶尔把 reasoning 写进 CONTENT 通道

#243 里你把 THINK_OPEN 定为 <think>,分类器整个机制建立在这个方言上。也就是说:两个 PR 同一作者、同一条泄漏,一个把 <think> 当作泄漏的标记,另一个的追踪表没有它。

我核过这不是你的疏漏 —— <think> 在 master 里根本不是标记字符串,是 #243 引进来的,所以 #249 这个分支引用不到。但两个 PR 是一批,合并后就会出现「追踪认不出被追踪的那个方言」。这条钓竿钓的是 <thinking>◁think▷,而现场最可能是 <think>

建议:在 THINK_MARKERS 里补 '<think>' / '</think>'。注释里那句「Extend here when another model ships a different marker」说明你已经预留了这个位置,这里只是把已知的那个填上。

M2(请核)— settle 埋点在异常出口上读空对象

chat.js 的 settle 日志在每一次循环退出时都会走到,包括 error 和 abort 路径 —— 而那些路径上 lastOkSr 是 null。我没有构造出崩溃(字段读取都带 ?.),但日志行在异常出口会输出一组全空字段,而那正是最需要现场数据的时刻。

请确认这是有意的还是漏了:如果异常出口也该记,字段应该带上「为什么是空」(finish 事件缺失?流被中止?);如果不该记,那个分支应该跳过。一条全空的 LEAK_TRACE 行比没有更糟 —— 它看起来像"追踪跑了但什么都没发现"。

M3(nit)— .env.example 那段没说日志量级

门开之后是逐事件记录,一个长回答会产生几十到上百行。运维打开它去抓现场时,应该预先知道量级和该怎么关(尤其它默认关是对的,但打开的人往往是在生产上打开)。建议在那段里加一句实测的量级,比如「一个 2000 token 的回答约产生 N 行」。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants