feat(devin-connect): session fidelity for multi-turn agentic work — stable ModelConfig + reasoning continuity (opt-in, default OFF) - #242
Conversation
我把"日志行被删"记成声明漏网,理由是"断言日志输出需要 harness"。核了一遍 不成立:`test/retry-rescue-budget-split.test.js:198` 从第十一轮起就在替换 `log.warn` 收集输出。这和 PR #242 那条 settle-flush survivor 是同一个毛病 —— "工具还没建"而工具就在隔壁文件里。 补两条断言:失败时恰好一行日志、含工具名与原串;以及一条负对照(合法 payload 零日志),否则前一条在"无条件记日志"的版本上也会绿,而那会在每次 工具调用上刷屏运维。 保留数据与报告出来是两个独立性质,原缺陷两个都丢了。突变 7 条现在全 CAUGHT, 零声明漏网。
四处过期,前两处是本轮 gemini 复核顺带查出来的:
1. `docs/README.md` 首行把 master 声明成一个比 v3.9.19 高一位的 tag。该 tag
不存在,而 master 实际领先 v3.9.19 五个 commit —— 两个方向同时错,且与
交接 §6(专门讲那几个未发版 commit)直接矛盾。
2. 台账第十一轮那张"仍未 exhaustive 扫描"表**两行过期**,而交接 §3.3 指路让人
"取最后一个命中" —— 第十二、十三轮都没有自己的表,所以读者恰好落在这张上:
- `#235` 第十二轮就解了(`dashboard/api.js:1782` + `index.html:7417`),
正文写了"标注顺带解掉 #235",表没改
- `release.yml` 那行写"要等下一次打 tag",此后 v3.9.15…v3.9.19 五个 tag
都跑过,最近一次六个 job 全 success(含 #233 修的 x64)
**导航规则是对的,数据过期了 —— 而规则的正确性掩盖了数据的过期。**
3. 索引对台账的描述写着 "1400+ lines / thirteen rounds",而它自己的括号里承认
"已经过期过一次"。按 §7.5 的结构修法改成不引用数字、只指路。
4. 交接 §0 写"开着的 PR:无",而 #242/#243 在写下它之后 47 分钟就开了,#244 在
110 分钟后。
守卫第 14 条:任何 md 文档里 `master == vX.Y.Z` 形式的声明,必须对上真实 tag
(refs/tags 与 packed-refs 都读,且在两者都读不到时显式跳过而不是报干净 ——
一个报"没问题"的坏探针是本仓库最贵的复发错误)。刻意只钉"tag 是否存在",不钉
"master 是否应该等于它",因为"未发版 commit 随下次发布搭车"是合法且有文档的状态。
守卫写完立刻抓到**第二处** —— 我人工只找到一处。而第一版台账勘误把错误字符串
照抄了进去,于是守卫又抓到本文自己,与 §7.4 同形(扫原始 prose 分不清 claim 与
对 claim 的引用)。这次没给守卫加特例,改的是写法:**记录一条已修的错误声明时,
描述它、不要重贴它。**
台账第十四轮记本轮全部结论。
|
评审:T1/T2 那一半是好东西,但 T4 是无门的行为变更,而 PR 头写着 opt-in / default OFF。请把它拆出去。 我实跑过什么head
T1/T2 我按突变标准审了,承重。 钳那条尤其: M1(blocker)— T4 没有门,而且它扣住整个流
只要这一轮有 reasoning,全部 content 就被扣到流末尾一次性发出。 用户看到的是:thinking 实时流 → 长时间静默 → 答案整段砸下来。渐进式流式对每个 thinking 模型都没了,而 PR 头是:
这句话对 A、B 成立,对 T4 不成立。T4 是这个 PR 里唯一默认生效的行为变更,而它改的是所有部署、所有协议、所有 thinking 模型。 你要修的那个重复是真的,我不否认这一点。但代价不该是"有 reasoning 就不流式"。两条路:
我倾向 2,因为它让这个特性可以默认开,那才是你想要的位置。 M2(blocker)— 那条 survivor 是整个 PR 后果最大的一行
把 本仓库对声明漏网的口径是台账第八轮定的:可以接受,前提是给出让它无害的前提。你给的是"工具还没建" —— 那是"没守卫",不是"无害"。对比你 spec 里其余十条,每条都真的咬在行为上,这一条是唯一的例外,而它恰好是风险最大的那行。 如果 T4 按 M1 的路 2 改,这条也顺带好办:比对逻辑挪进 M3(请撤)— 两个 cache-probe 不该进主干
你量缓存这件事做得对,结论写进 PR 描述或 一条小的digest 不转义分隔符,模型自己的 reasoning 能提前关掉这个块: 自己的 key、自己的会话,所以不是安全漏洞,但 digest 里剥掉 另外 处置建议把 T4 拆成单独 PR,T1/T2 这一半我可以先合。 理由:T1/T2 已经达到可合并的质量,而 T4 需要重新做取舍(M1)加重新做守卫(M2),捆在一起的话前一半要等后一半。 拆开之后 T1/T2 请 rebase 到当前 master 再跑一次全量。你的 base 落后 6 个 commit;master( 两条和你直接相关的:
顺带一条方法论,因为它和你那条 survivor 是同一个形状:我这一轮也声明了一条"断言日志输出需要 harness,所以记为漏网",核完发现
|
#244 的报告者问"swe1.7 为什么不识图"。答案的一半是 `DEVIN_CONNECT_IMAGE_TAG` 不设就等于视觉整体关闭,而这个开关此前**只写在 docs/DEVIN-CONNECT-CUTOVER.md 里** —— `README.md` / `README.en.md` / `.env.example` 三处各 0 次命中。也就是说 用户要打开视觉,基本没法从他会读的任何文件里知道它存在;症状是发了图、模型像 没看见、而日志干净。 补进 `.env.example` 的分五组,每个默认值都从源码读出来而不是凭印象: - 视觉:总开关(已验证值 10,2026-07-06 抓包)+ 四个子开关。写明子开关只在总开关 打开时才被读到,以及 `IMAGE_TOOLDEF` **默认是开**的 - router 模型(ASSIGN_MODEL / ASSIGN_TAGS / ROUTER_MODELS):标注 tag 是推测值、 未经付费往返校准 - STOP_REASON_MAP:写明未映射的值会落进"干净结束",所以校准前不能信 - TOOL_DEF_TAGS / TOOL_DEF_SOLO - 三个诊断开关(DEBUG_META / WIRE_DUMP / DUMP_RAW),标注会捕获请求内容 **两处我第一版写错了,核解析代码时抓到**:`IMAGE_INNER_TAGS` 是位置式 `base64,mime`(默认 `1,2`),不是 key=val;`TOOL_DEF_TAGS` 的键是 `parameters`(别名 `schema`),我写的 `parameters_json` 不在接受列表里 —— 按原样配置会被静默忽略。这正是"给出的例子必须能真的跑"这条。 两个 README 各加一节「图片/视觉怎么开」,含那组实测:`IMAGE_TAG=10` 时 `swe-1-7` 与 `claude-sonnet-4-6` **各 +478 字节**,代理侧零按模型分支 —— 所以"一个模型能识图另一个不能"的差异在上游。这条是 #244 的实际答案。 守卫(第 15 条):**prose 文档里写到的每个 env 名,必须在 .env.example 里有一行。** 反向查而不是正向枚举 —— 正向要解析器(env 访问有四种形态),而对字符串字面量 撒网会返回几百个假阳性(错误码和枚举名长得一样)。它写完立刻抓到两个我人工 没找到的:`WINDSURFAPI_LS_RELEASE`、`WINDSURFAPI_NLU_RETRY`(后者是三态, 默认对 GLM/Kimi 开、对 Claude/GPT 关,而 README FAQ 一直在推荐它)。 守卫自测五条。理由:扫真实文件的断言在语料干净时**无论检测逻辑好坏都会绿**, 正是"不可能失败的测试"那个形状;而守卫**不能用它自己做突变验证** —— 改松它 不会让任何别的测试红,写成 spec 就是四条没有前提的声明漏网,即我本轮批评 PR #242 那条 survivor 的同一个毛病。所以改成拿合成输入驱动检测逻辑。 自测承重已验证:去掉剥引用块那步,"引用不算声明"那条立刻红。 门禁 3479 / 0(266 文件)。
…e 下 dwgx#15.1 稳定、dwgx#15.2 单调 (opt-in) 校准注释里写得很清楚:genuine devin.exe 一个会话内 dwgx#15.1 恒定、dwgx#15.2 逐轮递增 (capture 9501aa2c,九条请求 1..8)。我们此前每条请求都是新 uuid + 常量 1 —— 那是 dwgx#226 之前「网关无状态」时代的正确值;session reuse 上线后这个前提已经不成立。 - session-continuity:state 增加 configId/turnCount;resolve 的查找逻辑抽成 findExistingState(行为不变,28 条既有测试原样通过),新增只读的 getSessionModelConfig(callerKey, messages) —— 不建状态、不刷 lastSeen; commit 时递增 turnCount(commitIndex 幂等路径不重复计)。 - buildGetChatMessageRequest 接受 sessionModelConfig {id, turn},缺省仍是 {fresh uuid, 1, 4},逐字节兼容旧 wire。 - chat.js:session 命中且门禁开时随 connectParams 传入,日志带 turn 号。 - 门禁 DEVIN_CONNECT_MODEL_CONFIG_STABLE(默认 0,需要 SESSION_REUSE), .env.example 记录依据。 - 测试:+6(gate 矩阵、跨轮稳定、幂等不重复计、只读不建状态、wire 两种形态); 全套 3453 pass / 0 fail。 动机:dwgx#15 与 dwgx#16(session_id)形状不一致时,upstream 的 affinity/KV 行为 无从验证;对齐后可以用 dashboard cache_read 占比做 A/B。
…(Thinking-core T1+T2),外加 T4 出口去重 使命背景:thinking 模型走代理时「想了不做 / 越想越笨」的退化,根因是 round-trip 死路(A3 矩阵:dwgx#11/dwgx#9 被接受但不被消费)。本提交不赌 wire 复活,走已验证的文本 通道,把每一轮我们已发出的 reasoning 存进会话、下一轮以 system 后缀回注。 T1 —— 服务端 continuity(扩展 dwgx#226 的 session-state,不加新结构): - state 增加 reasoningTails 队列:commitAfterResponse(..., { reasoning }) 存当轮 reasoning 的尾部摘要(意图/结论在尾部;MAX_CHARS 同时是单条摘要上限与注入总预算, 默认 4000,0 关闭;COUNT 队列长度默认 5,上限 32;幂等 commit 不重复存)。 - 注入:getSessionReasoningTrail 只读、不建状态、不刷 lastSeen;预算内从新到旧取整条 摘要,装进 [Continuity checkpoint] 块(9router prior-art 的框架:是上下文不是指令、 不要重新推导、不要提及),chat.js 在 resolve 点挂进 connectParams.continuityTrail, buildGetChatMessageRequest 追加到 tag dwgx#2 system —— 唯一不会变成 assistant 轮的位置 (自反思回灌是退化主因,明确不做)。字节兼容:仅 dwgx#2 变长。 - 门禁:DEVIN_CONNECT_SESSION_REASONING_INJECT(默认关,需 SESSION_REUSE)。 T2 —— 入站 reasoning 作为补充来源: - anthropic-in:messages.js 在丢弃 thinking 块前摘取最后一轮 assistant 的 thinking, 经 context.incomingReasoning 传给 chat.js;redacted_thinking 保持纯丢弃(消费不了, 注释写明)。 - openai/responses-in:chat.js 在 tool-emulation 重拆历史之前扫最后一条 assistant 的 reasoning_content/reasoning。 - 提交时出站优先,入站兜底(r.sr.reasoning || incomingConnectReasoning)——覆盖客户端 resume、上游无 reasoning 的回合。 T4 —— anthropic 出口 thinking/text 去重(maintainer 已点名的未来去重点): - 流式:reasoning 在场时 content 先 hold,finish 时若与 reasoning 逐字相同则抑制文本块, 否则原样冲出;thinking 在前是上游的既定顺序,反序不 hold 直接发。 - 非流式:content === reasoning_content 时只留文本块(行动通道优先)。 - 动机:上游会把推理原样再发一遍,agentic 客户端看到两份会自我重复(退化形态之一)。 测试:+18(T1 环境钮/摘要/队列/预算/门禁/幂等/只读、wire 两种形态、T2 摘取与 redacted、T4 流式×3 非流式×2);全套 3471 pass / 0 fail。
- reasoning-continuity.json:8 CAUGHT + 1 条文档化的 SURVIVOR(chat.js 提交兜底 只有 handler 级集成能看到,单测层已覆盖摘取与存储)。 - tools/cache-probe*.mjs:附录 B 的 TTFB 缓存 A/B 探针(2026-08-04 实测用)。
…DIGEST_MAX_CEILING 的课后作业 `1e9` 能过 isFinite;不钳的话一条 30KB 的 reasoning 会整段骑进 system prompt, 正是 dwgx#241 评审里钉过的无界请求体失效形态。钳在 32000,与 DIGEST_MAX_CEILING 同值。 +1 断言 +1 突变;spec 现在 10 条(9 CAUGHT + 1 文档化 SURVIVOR)。
…istingState 的根锚分支 review 复查时发现重构把根锚分支的说明留在了创建侧,查找侧裸奔 —— 补一行指针。
使命是协议无关的:推理/文本逐字重复这个退化形态,anthropic 出口抓到的不等于只治 anthropic。统一流(chat.js)同时喂 openai 直连、anthropic messages、gemini、responses 四个出口,所以决策点上移: - src/reasoning-dedup.js:createStreamReasoningDedup —— 流式路径唯一决策点。 reasoning 必须直播(长自反思是使命要求,缓冲 thinking 是反模式),所以流里唯一 可抑制的是后到的 content:reasoning 在场时 content 先 hold,finish 时逐字相同则 抑制、不同则原样冲出。仅影响客户端可见流;accText/accThinking 保留全量视图, 下游(沉默回退、旁白扫描、cascade 历史、usage)行为零变化。 - chat.js:emitThinking 记账、emitContent hold、finish settle;wantJson 的终发同样 跳过逐字重复(退化回声不是 JSON)。 - toChatCompletion(non-stream):非流式有选择权 —— 重复时留文本(行动通道)、丢 reasoning 副本。与流式策略的不对称是物理约束所致,设计文档写明。 - messages.js 的 T4 机构整体删除(被根方案取代);T2 入站摘取保留,测试文件更名 messages-incoming-thinking.test.js。 - 测试:+6 helper 单测、+2 适配器 non-stream;mutate spec 换 4 条(10 CAUGHT + 2 文档化 SURVIVOR:chat.js settle 冲刷需要统一流 e2e 夹具,尚不存在)。
…ил 2 теста fractional cap в devin-connect-openai)
…=292 (docs-consistency-guard требует число)
…inking path, digest escape Reviewer dwgx: T4 (reasoning/content egress dedup) is a behavior change without a gate — cut it from this PR wholesale; it will move to its own PR. Cut (T4): - src/reasoning-dedup.js deleted (createStreamReasoningDedup root) - chat.js: remove settle-flush in streamResponse, holdOrPass chunk suppression, noteReasoning, wantJson verbatim-echo guard, reasoning-dedup import - devin-connect-openai.js: remove non-stream verbatimDup guard - test/reasoning-dedup.test.js + verbatim test in devin-connect-openai.test.js deleted - test/mutations/reasoning-continuity.json: drop 4 T4 mutations + dedup test entry (baseline 292 -> 286, verified by harness run) Cleanup: - tools/cache-probe.mjs, tools/cache-probe2.mjs deleted (scratch; read API_KEY from .env) - _incomingThinking duplication removed: single __-prefixed body carrier __incomingThinking (same convention as __route); chat.js reads body.__incomingThinking, context.incomingReasoning threading dropped - digestReasoningTail strips '[End of continuity checkpoint]' from model text so the model cannot close the checkpoint block early; test case added T1 (stable ModelConfig) and T2 (reasoning continuity) logic untouched.
…ed from devin-connect-openai.test.js)
923c3b7 to
cf02426
Compare
|
По всем пунктам ревью, head 我实跑过什么
M1(blocker)— T4 拆出去了T4 从这个 PR 里整体删除: M2(blocker)— survivor 关闭新 T4 PR 里比对逻辑整体住在 M3 — cache-probe 已撤
两条小的 — 都做了
Rebase 干净; |
…ne thinking accumulate
|
评审:T1/T2 我按突变标准审了,承重。请修两条 —— 一条是那个声明漏网的前提站不住,一条是 count 钳位有个小数洞。 我实跑过什么head
钳位那条我驱动了两个旋钮的全部边界:
M1(请修)— 那个声明漏网的前提不成立突变把 前提说的是「只有 handler 级集成才能看到」。我核了一遍,不成立:
所以缺的不是工具,是一条 fixture:驱动一个 按本仓库口径(台账第八轮),声明漏网可以接受,前提是给出让它无害的前提。「工具还没建」是「没守卫」,不是「无害」—— 这也是上一轮 M2 对同一条提的意见。三种漏网成因里,这条是「没有 fixture 覆盖某个合取条件」,处置是造那条 fixture,不是记 另外:这个回退有两个调用点( M2(请修)—
|
| 项 | 结果 |
|---|---|
| 把新 master merge 进本 PR | 零文本冲突 |
| 本 PR 全部 spec anchor | 全部仍恰好命中一次 |
retry-rescue-budget-split.json |
我这轮没碰它量的两个文件(retry-rescue-budget-split.test.js / devin-connect-openai.test.js),基数仍是 81 |
最后一条对上面 M4 有直接影响:基数碰撞仍然只是你和 #243 之间的事,我的合并没有加剧它。 你和 #243 各自从 81 起算(82 / 86)这个前提没变,所以那条协调照原样处理即可。
我改动的文件与本 PR 有重叠(chat.js / messages.js),但改的段位不同 —— 我在 repairToolCallArguments 一带(约 1030-1215)和 document 块翻译处,你在 :3032 与 session-continuity 侧。
PR-1: feat(devin-connect): session fidelity for multi-turn agentic work — stable ModelConfig + reasoning continuity (opt-in, default OFF)
中文 TL;DR
让多轮 agentic 会话在 devin-connect 上更接近 genuine devin.exe 的行为,减少质量退化。两个 opt-in 特性,
都由
DEVIN_CONNECT_SESSION_REUSE作主门控,默认关闭、关闭时逐字节不变:#15.1保持稳定、#15.2单调递增(对齐 genuine devin.exe的逐轮行为),不再每轮随机 UUID。
system 后缀
[Continuity checkpoint]回注,让推理链跨轮可用、减少重推与退化;入站 thinking也被捕获为来源(T2)。附带 T4 出口去重(reasoning 与 content 逐字重复时抑制其一)。
均为内存态、有界、无持久化、无合成签名、不依赖 roутер。
Why this matters (为什么这关系到质量,而不是锦上添花)
Thinking-модели (Kimi K2/K3, DeepSeek-reasoner, Claude extended-thinking, GLM) «думают» перед
ответом: цепочка рассуждений возвращается в
reasoning_content/ thinking-блоках. Эта цепочка —не мусор, а рабочий контекст модели. Официальные документации вендоров единогласны:
криптографической подписью, любое изменение текста инвалидирует подпись и ломает multi-turn +
tool-use. Блок должен возвращаться ровно в том виде, в каком пришёл.
(platform.claude.com/docs/en/build-with-claude/thinking; ограничение:
github.com/[Bug]: Anthropic API rejects "thinking blocks cannot be modified" in extended conversations openclaw/openclaw#24612 «thinking blocks cannot be modified».)
reasoning_contentобязан полностью передаватьсяобратно во всех последующих запросах; без этого API вернёт 400, а модель не сможет «продолжить
предыдущее рассуждение». Без tool calls — опционально.
(api-docs.deepseek.com/guides/thinking_mode)
reasoning_content, «as-is»; «thinking before answering improves performance on complexreasoning, code generation, and multi-step tool calling».
(platform.kimi.ai/docs/guide/use-kimi-k2-thinking-model)
Почему деградация неочевидна. Модель всё равно отвечает — ответ не падает с ошибкой. Но без
полной цепочки рассуждений она каждый ход «начинает думать заново»: теряет план, повторно
исследует уже изученное, дублирует действия и деградирует в качестве на длинных агентских задачах.
Это тихая деградация: её не видно в одном ответе, она видна только на дистанции (петли, повторы,
рост токенов, снижение качества). Поэтому её легко счесть «нормой» — и именно поэтому нужен
измеримый ориентир, а не субъективное впечатление.
Цель проекта. Это не хобби-прослойка, а рабочий инструмент, на который полагаются. Ориентир —
≥98% качества от прямого обращения к исходному API (native). Всё, что реверс-прокси теряет из
цепочки рассуждений, — это прямая потеря качества относительно native. Задача фичи — вернуть цепочку
рассуждений модели так, чтобы многошаговые агентские задачи работали так же, как нативно.
Summary
Two opt-in session-fidelity features keyed to the devin-connect session already introduced in #226,
behind
DEVIN_CONNECT_SESSION_REUSE. Both are inert (byte-identical) when the gate is off. Keyed tothe failure signature (multi-turn agentic quality degradation), not to any model name. This is one link
of the thinking-core chain; it composes with the think-text reroute PR (separate).
Feature A — stable ModelConfig (#15)
#15.1(config UUID) stable across a session and increments#15.2(turn counter) monotonically. Previously the gateway minted a fresh UUID each turn with a constant
turn field.
src/session-continuity.js: session state gainsconfigId(stable) +turnCount(monotonic);refactored resolve into
findExistingState(behavior-preserving).src/devin-connect.js: emit stable#15.1+ monotonic#15.2when the gate is on.src/handlers/chat.js: passconnectParams.sessionModelConfigon session hit.Feature B — reasoning continuity (T1/T2) + T4 egress dedup
[Continuity checkpoint]block on the next turn (text channel — upstream accepts but does notconsume 当前项目似乎会瞬间触发速率限制? #11/cursor 模型命名不一样好像用不了 #9 reasoning tags, so text is the working channel). Budget
..._MAX_CHARS, clamp32000 so a runaway value cannot bloat the system prompt.
source; openai/responses reasoning_content likewise) so resumed dialogs keep their reasoning.
(root decision point feeding all four egress protocols).
Safety
Test plan
test/session-continuity.test.js— +13 tests: stable configId, monotonic turnCount,reasoning tail store/inject, budget clamp (32000 ceiling), T2 capture, read-only trail lookup.
test/devin-connect.test.js— +4 tests: stable opus 4.7 似乎访问不了? #15.1 / monotonic opus 4.7 似乎访问不了? #15.2 wire shape.test/messages-incoming-thinking.test.js— new, 2 tests: T2 incoming thinking capture.(retry-rescue-budget-split baseline 83 on current master).
Branch is on current master (v3.9.19), applies cleanly.
Note
Both features are opt-in and inert by default; safe to merge behind the gate. They are combined in one
PR because the reasoning store builds on the session-state refactor (they share session-continuity.js);
split if you prefer. Rebased on current master (v3.9.19) — verified compatible with the digest-cap
floor fix (2451ec8) and the rescueThinkingOnly removal (866da63). Happy to iterate.