Repository navigation
dsh-cursor-subscription v0.6.3
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 withclaude-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.TIMEOUTis 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:
turnEndednow ends the read loop (lib/index.js, the
interactionUpdatebranch) when no MCP tool call is pending, so the step
finishes with a cleanstopthe moment the turn is over. The final
conversation checkpoint always arrives beforeturnEnded, 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.mjsgained
"Cursor adapter finishes the step when the server signals turn_ended". It
replays text → checkpoint →turnEndedand then only heartbeats, and asserts
astopfinish. 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
turnEndedis the turn terminator and that the newest checkpoint precedes it;
the fixed adapter stops there and reportsfinish/stop, no longer depending on
anENDSTREAMframe 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:
maxToolRoundsnow defaults to200instead 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-subscriptionThe 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-configRestart DSH manually afterwards (patchReload does not pick up new bundles or
client modules), then check:
- Settings → Cursor loads, with
v0.6.3next to the title. cursor-subscriptionappears in the model picker, and sign-in, usage and
model cards work as before.- A Cursor answer that used to be cancelled after a while now stays, and the
conversation moves on without aTIMEOUTretry.
Compatibility and known notes
- A stall that is not announced by
turnEndedis still a stall: the progress
watchdog keeps aborting it with a retryableTIMEOUT. Only a turn the server
declared finished is treated as finished. - Cursor's Agent protocol remains undocumented and changes over time; a
transport orCURSOR_ERRORfailure 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 与客户端模块),然后确认:
- 设置 → Cursor 能打开,标题旁显示
v0.6.3; - 模型选择器里出现
cursor-subscription,登录、用量与模型卡片照旧可用; - 以前会被取消的 Cursor 回答现在会保留下来,会话继续往下走,不再出现
TIMEOUT重试。
兼容性与已知说明
- 没有
turnEnded宣告的卡死仍然是卡死:进度看门狗照旧以可重试的TIMEOUT中止。
只有服务端明确宣告结束的一轮才被当作已结束。 - Cursor 的 Agent 协议仍是未公开接口且会变化;聊天链路上的传输错误或
CURSOR_ERROR通常意味着需要更新插件。