Skip to content

dsh-cursor-subscription v0.6.3

Choose a tag to compare

@orrinzeng orrinzeng released this 18 Sep 10:30
· 35 commits to master since this release

English | 中文

dsh-cursor-subscription v0.6.3

A finished Cursor answer no longer gets cancelled and re-analysed. The adapter
now ends the step on Cursor's turn_ended update instead of waiting for the
HTTP/2 stream to close, which the server does not reliably do.

Fixed

An answer that had already streamed was discarded and the step re-ran

  • Symptom: the conclusion appears in the conversation, then ~60 s later it
    is cancelled, the model "thinks again" and produces a new conclusion, and the
    same thing can repeat for several more steps.
  • Affected: every build up to and including 0.6.2. It needs the server to
    end a turn without closing the run, which is intermittent — the same session
    can complete most steps normally and hang on a few. Observed on
    2026-09-18 with claude-opus-5-high (plugin 0.6.2, DSH 0.1.6-alpha.2).
  • Cause: Cursor terminates each assistant turn with the turnEnded
    interaction update (field 14). The adapter decoded it but never acted on it,
    so the read loop kept waiting for the stream to end. When Cursor keeps the run
    alive with heartbeats instead of closing it, the progress watchdog
    (STREAM_PROGRESS_TIMEOUT_MS, 60 s) aborts the run with
    Cursor stream progress timeout: no content for 60000ms, classified as
    TIMEOUT. TIMEOUT is in DSH's retryable set, so DSH discarded the answer it
    had already streamed and re-ran the whole step — up to its retry limit.
  • Fix: turnEnded now ends the read loop (lib/index.js, the
    interactionUpdate branch) when no MCP tool call is pending, so the step
    finishes with a clean stop the moment the turn is over. The final
    conversation checkpoint always arrives before turnEnded, so the persisted
    session state stays current; a pending MCP tool call still waits for its
    checkpoint, keeping the live bridge resumable.

Evidence

The plugin's own session log for a Cursor turn (llm/retry records) shows four
steps where a full answer was streamed and then thrown away:

Step Answer Streamed for Silent for Result
14 1522 chars 19.8 s 81.8 s TIMEOUT: Cursor stream progress timeout: no content for 60000ms
16 405 chars 6.1 s 71.9 s same
22 2446 chars 12.7 s 71.9 s same
26 1124 chars 14.9 s 69.8 s same

Each TIMEOUT was immediately followed by llm/retry (retry: 1/5), which is
exactly the "conclusion cancelled, analysed again" behaviour.

Verification

  • Unit: tests/proto.test.mjs gained
    "Cursor adapter finishes the step when the server signals turn_ended". It
    replays text → checkpoint → turnEnded and then only heartbeats, and asserts
    a stop finish. Before the fix it fails with the real error
    (TIMEOUT: Cursor stream progress timeout: no content for 60ms); after it,
    72/72 tests pass.
  • Live: an A/B probe against the real Agent API (same model as the affected
    session) drove the tool-call → resume → text-answer path through both the
    pre-fix adapter and the fixed one. Both runs' frame order ends with
    TXT … CP(589B) | UPD:unknown | CP(589B) | UPD:turnEnded, confirming that
    turnEnded is the turn terminator and that the newest checkpoint precedes it;
    the fixed adapter stops there and reports finish/stop, no longer depending on
    an ENDSTREAM frame arriving.

The probe run itself received an ENDSTREAM({}) right after turnEnded, so the
silent variant was not reproduced live; it is covered by the unit test, and the
fix removes the dependency on that frame either way.

Changed

  • Default tool-round cap: maxToolRounds now defaults to 200 instead of
    64; the accepted range is unchanged (1–1000). A profile that already saved a
    value under Settings → Cursor keeps its stored value — only the default
    for a fresh section moves.
  • Tests: 72 in total — 43 agent-protocol, 8 panel-language contract,
    6 channel mount, 6 version label, 6 native fetch, 3 image input.

Before installing: pnpm's 24-hour release-age gate

pnpm 11+ ships a built-in minimumReleaseAge of 24 hours, so a range such as
^0.6.0 never selects a version published within the last day. Right after a
release, a plain dsh plugin --profile web add dsh-cursor-subscription resolves
to the newest release older than 24 hours. Either request the exact version,
or exempt the package by name in the profile's pnpm-workspace.yaml:

minimumReleaseAgeExclude:
  - dsh-cursor-subscription

The exemption keeps the gate active for every other package. See
AGENTS.md
for the full install and verification procedure.

Install or upgrade

# exact version (works regardless of the 24-hour gate)
dsh plugin --profile web add dsh-cursor-subscription@0.6.3

# or, once the release-age exemption above is in place
dsh plugin --profile web add dsh-cursor-subscription
dsh plugin --profile web update dsh-cursor-subscription

dsh plugin --profile web list dsh-cursor-subscription --depth 0
dsh --profile web --dump-config

Restart DSH manually afterwards (patchReload does not pick up new bundles or
client modules), then check:

  1. Settings → Cursor loads, with v0.6.3 next to the title.
  2. cursor-subscription appears in the model picker, and sign-in, usage and
    model cards work as before.
  3. A Cursor answer that used to be cancelled after a while now stays, and the
    conversation moves on without a TIMEOUT retry.

Compatibility and known notes

  • A stall that is not announced by turnEnded is still a stall: the progress
    watchdog keeps aborting it with a retryable TIMEOUT. Only a turn the server
    declared finished is treated as finished.
  • Cursor's Agent protocol remains undocumented and changes over time; a
    transport or CURSOR_ERROR failure on the chat path is expected to need a
    plugin update.

dsh-cursor-subscription v0.6.3

已经输出的 Cursor 回答不会再被取消重来。适配器现在收到 Cursor 的 turn_ended
就结束该步,而不再等 HTTP/2 流关闭——服务端并不总会关。

修复

已经流式输出的回答被丢弃,整步重跑

  • 现象:结论已经出现在会话里,约 60 秒后被取消,模型「重新分析」并给出新的
    结论,之后还可能再来一次。
  • 影响范围:截至 0.6.2 的所有构建。触发条件是服务端结束一轮却没有关闭 run,
    这个行为是间歇性的——同一个会话里大部分步正常,少数几步会卡住。2026-09-18
    在 claude-opus-5-high 上复现(插件 0.6.2,DSH 0.1.6-alpha.2)。
  • 根因:Cursor 用 turnEnded 交互更新(field 14)标记每一轮助手回答的结束,
    适配器解码了它却从未处理,于是读循环继续等流结束。当 Cursor 只用心跳维持
    run、不关闭流时,进度看门狗(STREAM_PROGRESS_TIMEOUT_MS,60 秒)会以
    Cursor stream progress timeout: no content for 60000ms 中止该 run,错误被归类为
    TIMEOUT;TIMEOUT 属于 DSH 的可重试集合,于是 DSH 丢弃已经流式输出的回答并
    重跑整步,直到重试次数用尽。
  • 修法:在 interactionUpdate 分支中,turnEnded 且没有待处理的 MCP 工具
    调用时直接结束读循环(lib/index.js),该步随即以干净的 stop 收尾。最后一版
    会话 checkpoint 总是先于 turnEnded 到达,因此持久化的会话状态不会过期;
    有待处理工具调用时仍等 checkpoint,保持 live bridge 可续跑。

证据

插件自身的会话日志(llm/retry 记录)里,一轮里有四步是「回答已完整输出又被丢弃」:

步 回答 流式时长 之后静默 结果
14 1522 字符 19.8 s 81.8 s TIMEOUT: Cursor stream progress timeout: no content for 60000ms
16 405 字符 6.1 s 71.9 s 同上
22 2446 字符 12.7 s 71.9 s 同上
26 1124 字符 14.9 s 69.8 s 同上

每次 TIMEOUT 之后紧跟一条 llm/retry(retry: 1/5),这正是用户看到的
「结论被取消、重新分析」。

验证

  • 单元测试:tests/proto.test.mjs 新增
    《Cursor adapter finishes the step when the server signals turn_ended》:
    回放 文本 → checkpoint → turnEnded,之后只发心跳,断言最终是 stop。
    修复前该用例以真实错误失败(TIMEOUT: Cursor stream progress timeout: no content for 60ms),修复后 72/72 通过。
  • 真实链路:一次针对真实 Agent API 的 A/B 探测(模型与出问题的会话相同)把
    「工具调用 → 续跑 → 文本回答」这条路径分别跑过修复前与修复后的适配器。两次运行的
    帧序列都以 TXT … CP(589B) | UPD:unknown | CP(589B) | UPD:turnEnded 结束,证明
    turnEnded 就是该轮的终止标记,且最新 checkpoint 先于它到达;修复后的适配器
    在此处就结束并返回 finish/stop,不再依赖 ENDSTREAM 帧是否到达。

该次探测运行恰好紧跟着收到了 ENDSTREAM({}),所以「服务端不关流」这一变体没有
在真实链路上复现;它由单元测试覆盖,而修复本身已经消除了对该帧的依赖。

其他变更

  • 工具轮次上限默认值:maxToolRounds 默认从 64 改为 200,可选范围不变
    (1–1000)。已在 设置 → Cursor 保存过该值的 profile 仍沿用已保存的值,只有
    全新配置的默认值发生变化。
  • 测试:共 72 个——协议 43、面板文案契约 8、通道挂载 6、版本号 6、
    native fetch 6、图片输入 3。

安装前必读:pnpm 的 24 小时发布年龄门限

pnpm 11 及以后内置了 24 小时的 minimumReleaseAge 供应链防护:形如 ^0.6.0
的版本范围永远不会选中最近 24 小时内发布的版本。所以在刚发布完之后执行
dsh plugin --profile web add dsh-cursor-subscription,解析到的是「发布超过 24 小时的
最新版本」。解决方式二选一:安装时显式指定版本,或在 profile 的
pnpm-workspace.yaml 里按包名豁免:

minimumReleaseAgeExclude:
  - dsh-cursor-subscription

按包名豁免只对这个包放行,其他依赖仍受该防护约束。完整的安装与验收流程见
AGENTS.md。

安装 / 升级

# 显式指定版本(不受 24 小时门限影响)
dsh plugin --profile web add dsh-cursor-subscription@0.6.3

# 或者先加上前面的按包名豁免,再用普通命令
dsh plugin --profile web add dsh-cursor-subscription
dsh plugin --profile web update dsh-cursor-subscription

dsh plugin --profile web list dsh-cursor-subscription --depth 0
dsh --profile web --dump-config

之后手动重启 DSH(patchReload 不会重载新增 bundle 与客户端模块),然后确认:

  1. 设置 → Cursor 能打开,标题旁显示 v0.6.3;
  2. 模型选择器里出现 cursor-subscription,登录、用量与模型卡片照旧可用;
  3. 以前会被取消的 Cursor 回答现在会保留下来,会话继续往下走,不再出现 TIMEOUT 重试。

兼容性与已知说明

  • 没有 turnEnded 宣告的卡死仍然是卡死:进度看门狗照旧以可重试的 TIMEOUT 中止。
    只有服务端明确宣告结束的一轮才被当作已结束。
  • Cursor 的 Agent 协议仍是未公开接口且会变化;聊天链路上的传输错误或
    CURSOR_ERROR 通常意味着需要更新插件。