Skip to content

feat(cursor): spawn 会话补齐无 botmux send 时的 transcript 兜底(对齐 codex) - #932

Open
deepcoldy wants to merge 2 commits into
masterfrom
feat/cursor-spawn-bridge-fallback
Open

feat(cursor): spawn 会话补齐无 botmux send 时的 transcript 兜底(对齐 codex)#932
deepcoldy wants to merge 2 commits into
masterfrom
feat/cursor-spawn-bridge-fallback

Conversation

@deepcoldy

Copy link
Copy Markdown
Owner

改了什么 & 为什么

botmux 自己 spawn 的 cursor 会话此前没有 transcript 兜底:structured bridge 的 allowlist 把 cursor 限制为 adopt-only420aa12fe 引入 cursor 桥时的范围),模型完成一轮却忘调 botmux send 时整轮静默——而 claude/codex 会从 transcript 收割最终回答补发。cursor 的排水器(cursor-transcript.ts)与共享 CodexBridgeQueue / 兜底闸门(bridge-fallback-gate.ts)早已就位,本次只补 spawn 侧接线:

  • structured-bridge-clis.ts:cursor 移入 STRUCTURED_BRIDGE_ALWAYS_CLI_IDS,删除 adopt-only 特例(isStructuredBridgeFallbackActive 去掉 adoptMode 参数,唯一调用方同步更新)
  • worker.ts spawnCli:新增 cursor 桥 attach 分支——resume 用已知 chatId 直接解析 JSONL attach(baseline-existing,历史不重放);fresh spawn 此时还没有 chatId,先武装 1s poller
  • worker.ts observeCursorCliSessionId:pid→store.db fd 观察到 chatId 后,除 persist 外同步调 codexBridgeNotifyCliSessionId(JSONL 未创建时挂 pending sid 走 late-attach;已 attach 时 first-attach-wins 直接返回)
  • worker.ts cursorLateAttachMode:补非 adopt 语义——fresh spawn 从字节 0 摄入(chatId 级 JSONL 只含本会话,即使 attach 前首轮已落盘也不会误吞),resume 基线在既有尾部之后;adopt 分支行为逐字不变(birthtime 判定 + 异常回退 baseline-existing)

兜底语义与 codex 完全一致(同一 emitReadyCodexTurns + send-marker 闸门):本轮已 botmux send 则压制;prose+BOTMUX_NOTHING_TO_SEND 哨兵取 prose 补发(做了活忘发的场景);纯哨兵按真静默处理,不打扰话题。

影响面评估

  • cursor 非 adopt 会话(新增路径):每轮 flush 时 mark 指纹 → transcript user 行匹配开轮 → text-only assistant 行关轮 → 闸门决定补发/压制。提交验证租约有界(20s/30s),JSONL 迟迟不出现时 mark 静默过期,不会误报
  • cursor adopt 会话:仅注释级变化,attach 模式判定原逻辑保留
  • 其它 structured CLI(codex/traex/coco/hermes/mtr/pi/grok):allowlist 判定结果不变;emitTurnTerminal 有 per-turn claim 去重,结构化终端与 screen-idle 终端双路发射与 coco/grok 现状同形
  • 已知边界(与改动前行为相同,不激活兜底)--continue 降级 resume(无 chatId 可观察);sandbox 下宿主读不到 ~/.cursor/projects JSONL

测试

  • pnpm build 通过
  • pnpm vitest run test/structured-bridge-clis.test.ts test/cursor-transcript.test.ts test/bridge-fallback-gate.test.ts → 93 passed
  • 全量 pnpm test → 950 files passed,仅 test/skill-doctor-command.test.ts 2 例失败;已在干净 origin/master worktree 复跑确认为环境相关的既有失败,与本改动无关
  • live 验证:pnpm switch:here && pnpm daemon:restart 后在飞书新开 cursor 话题实测(结果将补充在评论)

Made with Cursor

deepcoldy and others added 2 commits August 18, 2026 08:49
botmux 自己 spawn 的 cursor 会话此前没有 transcript 兜底:structured
bridge 的 allowlist 把 cursor 限制为 adopt-only,模型完成一轮却忘调
`botmux send` 时整轮静默(claude/codex 均会从 transcript 收割最终回答
补发)。cursor 的排水器与共享 CodexBridgeQueue/兜底闸门早已就位,本次
只补 spawn 侧接线:

- structured-bridge-clis: cursor 移入 ALWAYS 列表,删除 adopt-only 特例
  (isStructuredBridgeFallbackActive 去掉 adoptMode 参数)
- worker spawnCli: 增加 cursor 桥 attach 分支——resume 用已知 chatId 直接
  解析 JSONL attach(baseline-existing,历史不重放);fresh 先武装 1s
  poller,等 chatId 观察到位后 late-attach
- observeCursorCliSessionId: 观察到 chatId 后同步通知
  codexBridgeNotifyCliSessionId(已 attach 时 first-attach-wins 直接返回)
- cursorLateAttachMode: 补非 adopt 语义——fresh spawn 从字节 0 摄入
  (chatId 级 JSONL 只含本会话),resume 基线在既有尾部之后;adopt 分支
  行为不变(birthtime 判定)

兜底语义与 codex 完全一致(同一 emitReadyCodexTurns + send-marker 闸门):
本轮已 send 则压制、prose+哨兵取 prose 补发、纯哨兵按静默处理。已知边界
不变:`--continue` 降级 resume(无 chatId 可观察)与 sandbox(宿主读不到
JSONL)下兜底不激活,与改动前行为相同。

影响面:cursor 非 adopt 会话新增兜底路径;adopt 路径仅注释级变化;其余
structured CLI(codex/traex/coco/hermes/mtr/pi/grok)的 allowlist 判定
结果不变。

测试:pnpm build 通过;vitest structured-bridge-clis(5)/
cursor-transcript(22)/bridge-fallback-gate(66) 全绿;全量 pnpm test 除
skill-doctor-command 2 例环境相关既有失败(干净 master 同样失败)外全绿。

Co-authored-by: Cursor <cursoragent@cursor.com>
飞书实测暴露:spawn 的 cursor 会话第 1 轮兜底正常(attach 全量排水路径),
第 2 轮起模型不调 botmux send 时整轮静默。根因是 cursor-agent
(2026.08.11)的 agent-transcript JSONL 并非严格 append-only——每轮结束在
文件尾追加 {"type":"turn_ended"} 状态 footer,下一轮落盘时把 footer 截掉、
从其原字节位置重写(footer 挪至新文件尾)。排水器按字节 offset 增量读,
上一轮结束后 offset 停在 footer 之后,下一轮 user 行起始于 footer 原位置
(offset 之前),永远读不到 → 指纹匹配不上 → 20s 归因租约过期整轮丢弃
(live 日志:expired 1 structured head(s) without transcript start)。
adopt / resume 场景第 2 轮起同样受影响。

三处修复:
- cursor-transcript drainCursorTranscript:newOffset 永不越过尾部已解析的
  role-less status 行(完整行与无换行 tail 两条路径都处理);footer 每 tick
  重读约 40 字节、重解析为零事件,不会产生重复
- cursor-transcript 新增 cursorBaselineOffset:resume/重启重挂的 baseline
  同样回退到尾部 footer 之前,worker cursorBridgeAttach 接入
- codex-bridge-queue 新增 refreshUnstartedHeadAttributionLease:cursor 的
  mirror 可能到首个 assistant step(纯生成回合则整轮)结束才落盘 user 行,
  worker tick 在 CLI 忙时(非 adopt)每秒刷新未开始 head 的归因租约,20s
  倒计时实际从 CLI 空闲起算;吞 Enter 场景的僵死保护语义不变

影响面:drainCursorTranscript 与 baseline-existing-skip-tail 均为 cursor
专属路径(已核对全部调用方);queue 新方法仅 cursor tick 分支调用,其它
structured CLI 行为不变。

测试:新增按 live 复现逐字节构造的 footer 重写回归用例、baseline 回退用
例、租约刷新/过期语义用例;cursor-transcript(32)/codex-bridge-queue(75)/
structured-bridge-clis(5)/bridge-fallback-gate(66) 全绿;全量 pnpm test 除
skill-doctor 2 例既有环境失败外,group-join-shared-routing 与
doc-comment-daemon-concurrency 在全量并发下偶发超时、单独复跑均绿。

Co-authored-by: Cursor <cursoragent@cursor.com>
@deepcoldy

Copy link
Copy Markdown
Owner Author

Live 实测发现回归并已修复(d6bb8bf)

首次部署后飞书实测:spawn 会话第 1 轮兜底正常(attach 全量排水路径),第 2 轮起失效——worker 日志 expired 1 structured head(s) without transcript start

根因:cursor-agent(2026.08.11)的 agent-transcript JSONL 并非严格 append-only。每轮结束在文件尾追加 {"type":"turn_ended","status":"success"} 状态 footer,下一轮落盘时把 footer 截掉、从其原字节位置重写(footer 挪至新文件尾)。排水器按字节 offset 增量读:上轮结束后 offset 停在 footer 之后,下轮 user 行起始于 footer 原位置(offset 之前)→ 永远读不到 → 指纹匹配不上 → 20s 归因租约过期整轮丢弃。adopt / resume 第 2 轮起同样受影响(此前 adopt 场景即存在、未被发现)。

修复(均 cursor 专属路径,已核对全部调用方):

  1. drainCursorTranscriptnewOffset 永不越过尾部已解析的 role-less status 行(完整行 + 无换行 tail 两条路径);footer 每 tick 重读 ~40 字节、零事件,不产生重复
  2. 新增 cursorBaselineOffset:resume/重启重挂 baseline 回退到尾部 footer 之前
  3. refreshUnstartedHeadAttributionLease:cursor mirror 可能到首个 assistant step / 整轮结束才落盘 user 行,CLI 忙时每秒刷新归因租约(20s 倒计时从 CLI 空闲起算,吞 Enter 僵死保护不变)

测试:新增按 live 复现逐字节构造的 footer 重写回归用例等 10+ 例;相关 4 个测试文件 178 例全绿;全量除 skill-doctor 2 例既有环境失败外全绿(group-join-shared-routing / doc-comment-daemon-concurrency 为全量并发下偶发超时,单独复跑绿)。已重新部署 live daemon 复测中。

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.

1 participant