Repository navigation
Releases: orrinzeng/dsh-cursor-subscription
Release list
dsh-cursor-subscription v0.6.12
dsh-cursor-subscription v0.6.12
DSH 0.2.0-rc.2 stopped booting this plugin, and the plugin was not at fault in the way a missing API usually is: nothing it called had been removed. The release added a version-compatibility gate, and this build declared the wrong line.
What 0.2.0-rc.2 changed
Before a profile imports a plugin, DSH now compares every @deepseek-ai/dsh and @deepseek-ai/dsh-* range in that plugin's peerDependencies against the running runtime version:
semver.satisfies(runtimeVersion, range, { includePrerelease: true })One unsatisfied range denies the whole plugin, and a denied plugin is denied in the launcher's own copy of the composition — it never imports its module, and it contributes no rows. That second half is what made the symptom confusing. The profile's own cordis.patch.yml carries a config override for the cursor-subscription entry, so a denied plugin does not report "incompatible" and stop: it reports
dsh: [C:\Users\<user>\.dsh\profiles\web\cordis.patch.yml] patch: entry "cursor-subscription" not found
which reads like a broken profile, not like a refused plugin. The real message is on stderr above it:
dsh: warning: Plugin dsh-cursor-subscription@0.6.11 is incompatible with dsh 0.2.0-rc.2:
peerDependencies {"@deepseek-ai/dsh-client-locale":"0.1.0-rc.6", ...}
Two details of the check are worth keeping. @deepseek-ai/cordis and react are not @deepseek-ai/dsh-* peers, so they are never inspected; and includePrerelease: true means a prerelease range matches a prerelease runtime more loosely than a bare semver.satisfies would — the old >=0.1.0-rc.6 <0.2.0 bound on the Connection peer was in fact still satisfied by 0.2.0-rc.2. Only the ten exact pins were not.
What this build declares
Every DSH peer now names the line this build is tested against instead of a single prerelease:
| Peer | Was | Now |
|---|---|---|
@deepseek-ai/dsh-client-connection |
>=0.1.0-rc.6 <0.2.0 |
^0.2.0-rc.2 |
@deepseek-ai/dsh-client-locale, dsh-client-ui-primitives, dsh-client-ui-settings, dsh-client-ui-slots, dsh-client-ui-tool, dsh-credentials, dsh-settings, dsh-tools |
0.1.0-rc.6 |
^0.2.0-rc.2 |
@deepseek-ai/dsh-llm |
^0.1.2-rc.1 |
^0.2.0-rc.2 |
@deepseek-ai/dsh-client-runtime |
0.1.0-rc.6 |
removed |
@deepseek-ai/cordis |
4.0.1 |
~4.0.4 |
^0.2.0-rc.2 deliberately covers the whole 0.2.x line rather than pinning 0.2.0-rc.2. A pin would deny this plugin again on the next 0.2.0-rc.3, which is the failure this release exists to stop repeating.
@deepseek-ai/dsh-client-runtime is gone from the 0.2.0-rc line — its last published version is 0.1.1-rc.2, and no 0.2.0-rc.2 exists to satisfy. Leaving that peer behind would have denied the plugin on its own, whatever the other ten said, so it is removed from peerDependencies and from the client-side dsh.client.inject list. Its slots service now comes from the platform seed table: every first-party client bundle requires @deepseek-ai/dsh-client-ui-primitives exactly as this one does, and none of them declares it as a client external.
One real API change: the Connection route options
@deepseek-ai/dsh-client-connection dropped a parameter between the lines. 0.1.x declared
register(owner, channel, handler, options) // options = { authority: "loopback" }
handle(channel, handler, options)and 0.2.0-rc.2 declares
register(owner, channel, handler)
handle(channel, handler)The loopback pin is not a renamed option — it is gone. 0.2.0-rc.2 fences every registered channel through one service-level check (loopback or a configured trusted authority, then browser authentication), so there is no per-route authority left to state, and a fourth argument would be silently ignored rather than honoured. The plugin now reads the declared arity and passes the option only to a build that still advertises it, so the 0.1.x line keeps its narrower fence instead of appearing to have one.
On 0.2.0-rc.2 the account channel is therefore fenced by the deployment's own policy. On a default local install that is the same loopback-only fence as before, plus browser authentication; on a deployment that has configured trustedHosts, this channel now follows them like every other registered channel. That is a platform-imposed widening, not a choice this plugin made, and it is stated rather than hidden.
Verification
- Unit: 104 host-side tests pass against the 0.2.0-rc.2 package line. The two this release adds cover the Connection signature both ways: a 0.1.x-shaped registry still receives
{ authority: "loopback" }, and a 0.2.0-rc.2-shaped one — three declared parameters, no options — is registered correctly and is not handed the argument it would ignore, on both theregisterpath and therpc.handlefallback. - The gate itself:
evaluatePluginCompatibilityfrom@deepseek-ai/dsh-app-boot0.2.0-rc.2, called with this manifest and runtime0.2.0-rc.2, returns no incompatible peers. The same function on the 0.6.11 manifest reports the ten exact pins, matching the boot log. - Composition:
dsh --profile web --dump-configprints thecursor-subscriptionrow with an empty stderr — no compatibility warning and nopatch: entry ... not found. The row is attributed to the bundle layer and then to the profile patch, which is the layering that was failing. - Import and schema:
dsh --profile web --dump-config-schemaimports every composed module and collects this plugin's Config schema (replayHistoryEachStepincluded), with no diagnostic for its row. The fourunrecognized Loader tree carriererrors in that run belong to DSH's ownpreset-standard,preset-ptc,preset-minimalandpreset-cordisrows. - Mount, in the running session: with the plugin installed into a live
webprofile,POST /cursor-subscription/versionanswers 401 unauthorized while an unknown path answers 405 — the route exists and is fenced by Connection's browser authentication, which is the 0.2.0-rc.2registerpath running. The DSH process serving it had already been up for eleven minutes when the plugin was installed, and was not restarted.
Install or upgrade
dsh plugin --profile web add dsh-cursor-subscription@0.6.12
dsh plugin --profile web list dsh-cursor-subscription --depth 0
dsh --profile web --dump-configRestart DSH manually afterwards and check that Settings -> Cursor shows v0.6.12.
Compatibility and known notes
- This build declares the 0.2.0-rc line and no longer covers DSH 0.1.x. If you are still on a 0.1.x DSH, stay on 0.6.11: 0.6.12 will be refused by the 0.1.x composition the same way 0.6.11 is refused by 0.2.0-rc.2.
- To accept the risk for one exact pair without changing versions, grant an exemption —
dsh plugin --profile web allow-version dsh-cursor-subscription@<version> --dsh-version <dsh-version> --accept-risk. An exemption is exact on both sides: neither a plugin upgrade nor a DSH upgrade inherits it. - The runtime behaviour of the provider, the
Runprotocol, the tool bridge, the settings fields and the client panel are unchanged from v0.6.11. This release moves declarations and one registration call. - The repository's
pnpm-workspace.yamlnow lists the 0.2.0-rc.2 packages inminimumReleaseAgeExclude, so a freshpnpm installcan resolve them while they are still inside pnpm's 24-hour release-age window.
dsh-cursor-subscription v0.6.12
DSH 0.2.0-rc.2 让本插件起不来了,但这次不是"某个 API 被删掉"那种故障:插件调用的一切都还在。这个版本加了版本兼容闸门,而本插件声明错了版本线。
0.2.0-rc.2 改了什么
在 profile 导入一个插件之前,DSH 现在会把该插件 peerDependencies 里所有 @deepseek-ai/dsh 与 @deepseek-ai/dsh-* 的范围拿去和当前运行时版本比对:
semver.satisfies(runtimeVersion, range, { includePrerelease: true })只要有一个范围不满足,整个插件就被拒绝;而且被拒绝的插件是在启动器自己那份组合结果里被拒的——它根本不会被 import,也不会贡献任何条目。后半句正是症状令人费解的原因。profile 自己的 cordis.patch.yml 里带着一条针对 cursor-subscription 条目的配置覆盖,于是被拒的插件并不是报一句"不兼容"就停下,而是报:
dsh: [C:\Users\<user>\.dsh\profiles\web\cordis.patch.yml] patch: entry "cursor-subscription" not found
读起来像是 profile 坏了,而不是插件被拒。真正的信息在它上面一行:
dsh: warning: Plugin dsh-cursor-subscription@0.6.11 is incompatible with dsh 0.2.0-rc.2:
peerDependencies {"@deepseek-ai/dsh-client-locale":"0.1.0-rc.6", ...}
这个检查有两个细节值得记住。@deepseek-ai/cordis 与 react 不属于 @deepseek-ai/dsh-*,永远不会被检查;而 includePrerelease: true 让预发布范围比裸 semver.satisfies 更宽松地匹配预发布运行时——Connection 那条旧范围 >=0.1.0-rc.6 <0.2.0 其实仍然满足 0.2.0-rc.2。真正不满足的是那十条精确钉死的声明。
本版本声明了什么
每个 DSH peer 现在声明的是本构建实测的版本线,而不是某一个预发布版本:
| Peer | 原值 | 现值 |
|---|---|---|
@deepseek-ai/dsh-client-connection |
>=0.1.0-rc.6 <0.2.0 |
^0.2.0-rc.2 |
@deepseek-ai/dsh-client-locale、dsh-client-ui-primitives、dsh-client-ui-settings、dsh-client-ui-slots、dsh-client-ui-tool、dsh-credentials、dsh-settings、dsh-tools |
0.1.0-rc.6 |
^0.2.0-rc.2 |
@deepseek-ai/dsh-llm |
^0.1.2-rc.1 |
^0.2.0-rc.2 |
@deepseek-ai/dsh-client-runtime |
0.1.0-rc.6 |
移除 |
@deepseek-ai/cordis |
4.0.1 |
~4.0.4 |
^0.2.0-rc.2 是刻意覆盖整条 0.2.x 线,而不是钉死 0.2.0-rc.2。钉死的话,下一个 0.2.0-rc.3 会再次拒绝本插件——那正是本版本要终结的那种重复故障。
@deepseek-ai/dsh-client-runtime 在 0.2.0-rc 线里已经不存在:它最后发布的版本是 0.1.1-rc.2,没有 0.2.0-rc.2 能满足。把这条 peer 留下,光它自己就足以拒绝整个插件,无论另外十条写什么;因此它同时从 peerDependencies 和客户端 dsh.client.inject 列表里移除。它提供的 slots 服务现在来自平台种子表:所有一方客户端包都像本插件一样 require("@deepseek-ai/dsh-client-ui-primitives"),没有任何一个把它声明成 client external。
一处真实 API 变更:Connection 的路由选项
@deepseek-ai/dsh-client-connection 在这两条线之间少了一个参数。0.1.x 声明的是
register(owner, channel, handler, options) // options = { authority: "loopback" }
handle(channel, handler, options)
...dsh-cursor-subscription v0.6.11
dsh-cursor-subscription v0.6.11
Two fixes for the same symptom, and a switch for the structural cause of it. A Cursor turn was observed re-deriving its own plan instead of working: the step restated its goal, read its goal back, rewrote its plan, and never moved past the first item — while every tool call it made returned real data.
A replay keeps what each request asked for
A cold start — a process restart, a lost bridge, or the rebuild that follows a compaction — replays the DSH history as text, and a tool request used to replay as [Previous tool request: todo_write] and nothing else. For a plan the request is its argument, so the model came back without the list it had just written and re-derived one; the same gap hides the search it already ran, which is how a turn re-issues a call it has already made. Each replayed request now keeps its arguments up to 600 characters: a plan survives whole, a whole-file write stays bounded.
The churn this addresses, measured on one session: 15 todo_write calls, none identical to the one before it, with a turn that rewrote the same three-item plan in three consecutive steps while its first item stayed in_progress — and one rewrite that came out in English. Plan churn is mostly a symptom of a turn that is stuck: the plan tool replaces the entire list on every call and answers with a count rather than the list, so any edit is a retype, and an agent that cannot make progress edits the one thing it still controls.
An opt-in rebuild that re-sends the history each step
dsh-codex-subscription and this plugin differ in one structural way, and it is the reason a Codex model plowed through 114 steps of a session that stalled a Cursor model: the Codex plugin speaks standard function calling, so its adapter assembles the message history itself and every request carries it, while this adapter delegates history to Cursor's server and sends deltas. The tool surface follows from that: the Codex model only ever sees DSH's tools, while this one is also offered Cursor's own, which have to be refused.
Replay the full history each step (Settings → Cursor, off by default) makes every step rebuild the conversation from the DSH history instead of resuming the conversation Cursor keeps, so the model sees the same explicit history a function-calling adapter gives it. It is opt-in because Cursor has no provider-side compaction: the whole transcript is re-sent each step, so tokens grow with the step count.
The turn that motivated it carried get_goal four times, todo_write twice and a re-stated goal in every step's text, while the grep, glob and read calls in those same steps all returned real data — and no native tool was attempted at all (0 mid-message round trips across 24 steps), which rules out the refused-tool path as the cause.
Verification
- Unit: 101 host-side tests pass. The two cases this release adds: a cold-start action text keeps a
todo_writerequest and the list it carried, keeps the result that answered it, and truncates awritewhose body is 5,000 characters instead of replaying it; and with the switch on, a step whose tool result is in hand starts a fresh run, releases the bridge it does not resume, carries DSH's history and that result into the new run, and answers no abandoned exec, while the settings RPC round-trip covers the new field and still drops an unknown key. - Unchanged paths: the resume path, the sandbox-refusal hints, the protocol progress lines and the tool-result handling from v0.6.10 all keep their tests green.
- The churn, read from a real session: 15
todo_writecalls with no two consecutive lists identical, three consecutive rewrites of one three-item plan, and one rewrite in English. - The stall, read from a real session: in a turn the user aborted,
get_goalfour times plustodo_writetwice, withgrep,globandreadall returning data in the same steps and no native tool request attempted.
Install or upgrade
dsh plugin --profile web add dsh-cursor-subscription@0.6.11
dsh plugin --profile web list dsh-cursor-subscription --depth 0Restart DSH manually afterwards and check that Settings → Cursor shows v0.6.11 and that the runtime-settings card lists Replay the full history each step.
Compatibility and known notes
- The rebuild switch trades tokens for explicitness: with it on, a step's prompt grows with the whole transcript, and nothing on Cursor's side compacts it. It changes no other behaviour — the same tool calls, the same permissions, and the server-side conversation is simply not resumed.
- Replayed requests keep at most 600 characters of arguments. A plan fits whole; a file body does not, and its result is in the transcript anyway.
- The switch is the only new setting. The run stream, the Config schema besides that field, and the RPC surface are unchanged from v0.6.10.
- Turning the switch on is an experiment, not a default: it is the one place where this adapter can be brought closer to how a function-calling plugin behaves, and it costs tokens per step. If a turn still re-derives its plan with it on, the cause is not the delegated history.
dsh-cursor-subscription v0.6.11
两处修复针对同一个症状,另加一个针对其结构性原因的开关。观察到的现象是:Cursor 的一轮任务在反复重推自己的计划,而不是干活——每一步都重述目标、把目标读回来、重写计划,永远停在第一项,而同一步里的工具调用每一次都返回了真实数据。
重放会保留每次请求的内容
冷启动——进程重启、桥断、或压缩之后的重建——会把 DSH 历史重放成文本,而工具请求过去只重放成[Previous tool request: todo_write] 一个名字。对计划来说,请求就是它的参数,于是模型回来时手里没有自己刚写的那张表,只能重新推导一张;同一个缺口也藏起了它已经跑过的检索,这就是同一个调用被重复发起的来源之一。现在每条重放的请求都保留参数(上限 600 字符):计划完整保留,整文件write 则被截断。
这个改动针对的抖动,取自真实会话:15 次 todo_write,没有一次与上一次逐字相同;某一轮连续三步重写同一份三项计划,而第一项始终停在 in_progress,其中一次还变成了英文。计划抖动主要是"卡住"的症状:计划工具每次调用都整表替换、结果只回计数不回显清单,于是任何改动都是重打一遍;一个推进不下去的 agent,就会去改它唯一还能控制的东西。
可选的"每步重放完整历史"
dsh-codex-subscription 与本插件只在一处结构上不同,而它正是"同一个会话里 Codex 模型跑完 114 步、Cursor 模型却卡住"的原因:codex 插件走标准 function calling,历史由它自己组装、每次请求自带;本插件把历史交给 Cursor 服务端,只发增量。工具面也是同一个差别的结果:Codex 模型只会看到 DSH 的工具,而本插件这边还会被提供 Cursor 自己的工具,只能回绝。
「每步重放完整历史」(设置 → Cursor,默认关闭)让每一步都从 DSH 的历史重建会话,而不是沿用Cursor 服务端保存的会话,模型因此拿到与 function calling 适配器同等的、显式的完整历史。之所以默认关闭:Cursor 没有服务端压缩,整段记录会每步重发,token 随步数增长。
触发这个改动的那个 turn:get_goal 4 次、todo_write 2 次,每一步正文都重述目标;而同一步里的 grep、glob、read 全都返回了真实数据——并且 24 步中一次原生工具都没试过(中途往返0/24),这排除了"被回绝的工具"这条路径。
验证
- 单元测试: Host 侧 101 项全部通过。本版本新增的两项用例:冷启动的 action 文本里保留
todo_write请求及其清单、保留答复它的结果,并且对正文 5,000 字符的write只做截断、不重放全文;以及开关打开时,手里已有工具结果的那一步会新建一个 Run、释放它不复用的桥、把 DSH历史与该结果带进新 Run、并且不去答复被放弃的 exec,同时设置 RPC 往返覆盖该字段并仍然丢弃未知键。 - 未受影响的路径: v0.6.10 的 resume 路径、沙箱拒绝提示、协议进度行与工具结果处理,测试全部保持通过。
- 抖动(取自真实会话): 15 次
todo_write,没有相邻两次清单相同;同一份三项计划被连续重写三次;其中一次变成英文。 - 卡死(取自真实会话): 在一轮被中断的 turn 里,
get_goal4 次 +todo_write2 次,同一步的grep、glob、read都返回了数据,且没有发起任何原生工具请求。
安装或升级
dsh plugin --profile web add dsh-cursor-subscription@0.6.11
dsh plugin --profile web list dsh-cursor-subscription --depth 0之后手动重启 DSH,确认 设置 → Cursor 显示 v0.6.11,并且运行设置卡片里出现**「每步重放完整历史」**。
兼容性与已知事项
- 重建开关是"用 token 换显式上下文":打开后每一步的 prompt 都随整段记录增长,Cursor 侧不会做任何压缩。除此之外不改变行为——同样的工具调用、同样的权限,只是不再复用服务端会话。
- 重放的请求最多保留 600 字符参数。计划能整份保留;文件正文不能,而它的结果本来就在对话记录里。
- 新增设置只有这一个字段。除此之外,运行流、Config schema 与 RPC 面与 v0.6.10 一致。
- 打开这个开关是实验,不是默认值:它是本适配器唯一能向 function calling 插件靠拢的地方,代价是每步的 token。如果打开后某一轮仍在重推计划,那原因就不在"历史被委托出去"这件事上。
dsh-cursor-subscription v0.6.10
dsh-cursor-subscription v0.6.10
Two things a Cursor turn kept to itself: the work it was doing while it ran, and
the way out of a refused tool call. A step showed one sentence and a tool card,
so a turn of them read as the same request being submitted again and again; and
a sandbox refusal arrived as a bare error, so the model concluded that local
tools do not work here instead of asking the user for permission.
The work behind a step is visible now
Reasoning is the only channel an adapter has for showing work that is not answer
text. A Cursor step spends most of its wall clock on protocol round trips that
never reached the transcript: the native tool call the model tries first, the DSH
tool-schema fetch it makes next, and the thinking it finished before either. None
of that was visible, and the four deltas below now report it, prefixed [cursor]
on a reasoning block of their own (index 3) so the model's own thinking — block
index 1 — stays exactly what the model produced.
| when | what the transcript shows |
|---|---|
| the model finishes thinking | [cursor] thinking finished after 4.2s |
| a native Cursor tool is declined | [cursor] native grep call declined; using the DSH tools instead |
| the tool schemas were fetched | [cursor] loaded the DSH tool schemas (27 tools) |
| a DSH tool is about to run | [cursor] calling the DSH tool grep |
thinking_completed (InteractionUpdate field 5) carried the duration all
along and was ignored as an unknown update; it is decoded now. The other three
lines report events the adapter already handled and only ever answered on the
wire.
A sandbox refusal carries the way to ask for approval
Every provider gets the same rule: when the sandbox refuses a call, retry that
exact call once with sandbox_permissions and a justification, and DSH raises an
approval prompt. Only the pwsh description spells it out — write, edit and
read carry 42, 59 and 56 characters of description with no such wording, and
the assembled system prompt says nothing about the sandbox at all. The refusal
result itself carried no remedy either; the one a Cursor turn received was:
Error: SetNamedSecurityInfoW failed (Win32 5): grantWrite(D:\myworks\mkgame_front)
A model that does not recall the rule from a tool description reads that as "this
tool does not work here" and stops using it — which is how a turn ends up
reporting 本地工具不可用 and switching to search-only work. It is not that the
model cannot escalate: the same build was observed escalating eight times in one
session, first to workspace-write (refused by the ACL grant above) and then to
danger-full-access, which ran.
withSandboxHint() now appends the rule to a refusal, in both places a result
travels back to the model — the live-bridge mcp_result and the cold-start
history replay. The two refusal families read differently, because they call for
different answers. A policy refusal keeps the narrowest-first rule:
[cursor] The DSH sandbox refused this call, and that is recoverable: retry this
exact call once with sandbox_permissions set to the narrowest wider mode that
suffices (workspace-write, or danger-full-access when that is not enough) plus a
one-sentence justification. DSH then asks the user to approve it. If the retry is
refused as well, stop and report it instead of trying another mode.
An ACL failure says plainly that no narrower mode can work, because naming one
first is what turned a single refusal into a retry loop — in one session a turn
asked for workspace-write, the mode it was already in, 101 times:
[cursor] The DSH sandbox cannot start in this workspace — its own ACL grant fails
(`SetNamedSecurityInfoW`) — so no narrower mode can succeed. Retry this exact call
once with sandbox_permissions="danger-full-access" and a one-sentence
justification; DSH then asks the user to approve it. Do not retry it in a
narrower mode, and if the retry is refused, stop and report the refusal instead
of trying again.
The reader gets a line too: [cursor] the sandbox refused a tool call; told the model how to ask for approval.
Verification
- Unit: 99 host-side tests pass. New cases: the four progress lines arrive in
order, keep their[cursor]prefix, leave the model's own thinking on its own
block, and still hand the agent loop a real tool call;thinking_completed
decodes to the duration Cursor reported; and with three results in flight — an
ACL failure, a policy refusal and an ordinary result — each refusal is answered
exactly once, the ACL failure namesdanger-full-accessand forbids the
narrower retry, the policy refusal keeps the narrowest-first rule, neither
invites an unbounded loop, and the reader's line is emitted per refusal. - What Cursor actually receives, read back from a live session: the
pwsh
description is 3,156 characters and states the escalation rule;write,edit
andreadare 42, 59 and 56 characters with no wording about it; the assembled
system prompt is 7,816 characters with no mention of the sandbox or approvals. - What a refusal looks like today, read from the same session: the result
carried only the ACL error above, which is the text the hint now follows. - The loop this release ends, read from a
D:session: 101 ACL failures and
49 escalations in one transcript, with a turn spending 80 tool calls on the
sameworkspace-writeretry until the user aborted it.
Install or upgrade
dsh plugin --profile web add dsh-cursor-subscription@0.6.10
dsh plugin --profile web list dsh-cursor-subscription --depth 0Restart DSH manually afterwards and check that Settings → Cursor shows
v0.6.10.
Compatibility and known notes
- Progress lines are transcript content: they replay in a cold start and appear
in DSH's compaction summaries. The model's own thinking is untouched — the two
kinds of reasoning sit on different block indexes. - The escalation rule is appended only to a real sandbox refusal
([sandbox: … denied]or theSetNamedSecurityInfoWACL failure) whose text
does not already mentionsandbox_permissions. An ordinary tool error is left
exactly as it was. - Cursor's native tools are still declined rather than executed; the transcript
now says so, and says what to do instead. - On a
D:workspace theworkspace-writesandbox cannot grant its own ACL
(SetNamedSecurityInfoW failed (Win32 5)), which is why the hint names
danger-full-accessas the mode that suffices when a narrower one does not. - No new endpoints and no settings change: the run stream, the Config schema and
the RPC surface are unchanged from v0.6.9.
dsh-cursor-subscription v0.6.10
Cursor 任务有两件事一直藏着:跑的时候它在做什么,以及工具被拒之后该怎么办。每一步只显示一句
话加一张工具卡,于是一整轮看起来像同一个请求被反复提交;而沙箱拒绝只回一句干巴巴的错误,模型
因此认定"本地工具不可用",而不是向用户申请权限。
每一步背后做的事现在看得见了
reasoning 是适配器唯一可以承载"非正文内容"的通道。一个 Cursor 步骤的大部分墙钟时间都花在协议
往返上,而这些往返从未进入对话记录:模型先试的原生工具、随后拉取的 DSH 工具清单、以及在这两
件事之前结束的思考。这些以前全都不可见;现在由下面四条增量上报,统一带 [cursor] 前缀,并且
占用独立的 reasoning 块(索引 3)——模型自己的思考仍在索引 1,保持原样。
| 时机 | 对话里显示的内容 |
|---|---|
| 模型结束思考 | [cursor] thinking finished after 4.2s |
| 原生 Cursor 工具被回绝 | [cursor] native grep call declined; using the DSH tools instead |
| 拉取到工具清单 | [cursor] loaded the DSH tool schemas (27 tools) |
| 即将发起 DSH 工具调用 | [cursor] calling the DSH tool grep |
thinking_completed(InteractionUpdate 字段 5)一直带着思考时长,但此前被当作未知更新整条
忽略;现在解出来了。另外三条对应的事件适配器本来就在处理,只是从来没有体现在对话里。
沙箱拒绝会带上"怎么申请权限"
所有 provider 拿到的规则是一样的:沙箱拒绝某个调用后,用 sandbox_permissions 加一句
justification 重试同一个调用,DSH 就会弹出审批。但只有 pwsh 的描述写明了这条规则——
write、edit、read 的描述分别只有 42、59、56 个字符,完全没提;拼装出的系统提示里也
一个字都没有。而拒绝结果本身也不带补救办法,Cursor 那一轮实际收到的只是:
Error: SetNamedSecurityInfoW failed (Win32 5): grantWrite(D:\myworks\mkgame_front)
不记得工具描述里那条规则的模型,会把这句话读成"这个工具在这里不能用",从此不再使用它——这正是
某一轮里出现 本地工具不可用、然后退化成只做检索的原因。并不是模型不会申请:同一个版本在另一个
会话里被观察到连续申请 8 次,先提 workspace-write(被上面那个 ACL 授权失败挡回),再提
danger-full-access,随后全部执行成功。
现在 withSandboxHint() 会把这条规则附加到拒绝结果上,覆盖结果回传给模型的两条路径——
live bridge 的 mcp_result 与冷启动的历史重放。两类拒绝的措辞不同,因为它们要的答案不同。
策略拒绝沿用"从最窄的更高模式开始":
[cursor] The DSH sandbox refused this call, and that is recoverable: retry this
exact call once with sandbox_permissions set to the narrowest wider mode that
suffices (workspace-write, or danger-full-access when that is not enough) plus a
one-sentence justification. DSH then asks the user to approve it. If the retry is
refused as well, stop and report it instead of trying another mode.
ACL 失败则直接说明更窄的模式不可能成功——正因为先报了最窄模式,一次拒绝才变成了重试循环,
某个会话里有一轮就那样重复申请了 101 次 workspace-write(也就是它当时已经在用的模式):
[cursor] The DSH sandbox cannot start in this workspace — its own ACL grant fails
(`SetNamedSecurityInfoW`) — so no narrower mode can succeed. Retry this exact call
once with sandbox_permissions="danger-full-access" and a one-sentence
justification; DSH then asks the user to approve it. Do not retry it in a
narrower mode, and if the retry is refused, stop and report the refusal instead
of trying again.
读者这边也有一行:[cursor] the sandbox refused a tool call; told the model how to ask for approval。
验证
- 单元测试: Host 侧 99 项全部通过。新增用例覆盖:四条进度行按序到达、都保留
[cursor]
前缀、模型自己的思考仍在独立块上、真实工具调用照旧进入 agent 循环;thinking_completed
能解出 Cursor 上报的时长;以及一次交回三个结果(ACL 失败、策略拒绝、普通结果)时,两个拒绝
各只被答复一次、ACL 失败改写danger-full-access并禁止更窄模式重试、策略拒绝保留
"最窄优先"、两者都不允许无界重试,且每次拒绝都发出给读者的那一行。 - 回读 Cursor 实际收到的内容(取自真实会话):
pwsh描述 3,156 字符且写明提权规则;
write、edit、read分别为 42、59、56 字符,只字未提;拼装的系统提示 7,816 字符,
没有出现 sandbox 或 approval 字样。 - 回读当下一次拒绝的实际形状(同一会话): 结果里只有上面那句 ACL 错误——也就是现在会
...
dsh-cursor-subscription v0.6.9
dsh-cursor-subscription v0.6.9
v0.6.9 — DSH 0.1.7 compatibility, compaction, honest usage
DSH 0.1.7-alpha.1 replaced the settings service this plugin was built against.
apply called ctx.get("settings").installSection(...), the method no longer
existed, and the cursor-subscription entry failed to activate on every boot:
cursor-subscription (dsh-cursor-subscription):
TypeError: ctx.get(...).installSection is not a function
The same session records showed two more defects on the Cursor path: a run kept
the transcript DSH had just compacted away, and the adapter reported placeholder
token counts.
Root cause
Three separate contracts had drifted.
The settings API. DSH 0.1.7 composes a plugin's settings page from its own
profile entry: the entry id is the namespace, only fields marked .volatile()
appear, the Loader delivers those as live references (config.x.get()) that it
updates in place, and settings.configure({ auto }, fiber) states the page
policy. installSection and settings.register — the section-registration pair
this build was written against — are gone. A Config without a volatile field
produces no section at all, and .volatile() itself first exists in
@deepseek-ai/schemastery 3.18.3, which this plugin declared as a peer under a
range that still allowed 3.18.1.
Compaction. DSH frees context by replacing a span of the conversation with a
summary and marking that message source.kind = "compact-checkpoint" with the
id of the compaction. The adapter preferred its own persisted Cursor checkpoint
whenever one existed, and that checkpoint still described the pre-compaction
transcript: in the failing session DSH had condensed the history to ~2.4k tokens
while the model kept running on ~204k tokens of history DSH had already dropped.
The run re-planned the same investigation step after step — 22 steps, one search
each, no file ever edited — until the user aborted it.
Usage. inputTokens was hardcoded to 0, and outputTokens kept the last
token_delta frame instead of summing the deltas, so a full paragraph was
reported as 1–3 tokens.
Fixed
The settings section follows the DSH 0.1.7 API. Config fields are now
.volatile(), readConfigField() reads a field that may be either a live
reference or a plain value, and apply states the page policy through
ctx.inject(["settings"], ...) → settings.configure({ auto: false }, ctx.fiber):
this plugin ships its own Settings → Cursor panel, so the generated form
stays off. On a DSH release without configure (the 0.1.5 line) the Cursor
provider still loads and only the settings page stays empty, instead of failing
the whole entry at boot.
@deepseek-ai/schemastery is a runtime dependency. It moved from
peerDependencies to dependencies (^3.18.3), the way DSH's own plugins
declare it, so an install carries the library version that has .volatile() and
a stale shared node_modules tree can no longer decide which library parses the
plugin's Config.
A compaction drops the checkpoint it invalidates. compactionIdOf() reads
the compaction id out of the message history. When it differs from the id the
persisted Cursor conversation was built from, the adapter drops the checkpoint
and its blobs and refuses to resume a live bridge across it, so the next cold
start is rebuilt from what DSH kept.
Usage reports the real prompt size and the whole output. The prompt size
Cursor reports rides in the conversation checkpoint
(ConversationStateStructure.token_details.used_tokens) and is now decoded by
decodeCheckpointUsedTokens(); output tokens are summed from the token_delta
frames.
v0.6.8 — a tool request this build cannot name is still answered
A Cursor turn that needed a tool could end right after the model said what it
was about to do — "I'll read that file" — with nothing actually happening. The
model had asked Cursor for one of its built-in tools, this build did not
recognize the request, and because nothing answered it the server waited for a
result that never came: the progress watchdog fired 60 seconds later and only
the preamble text survived into the conversation.
An exec this build cannot name is still answered. ExecServerMessage
carries Cursor's tool requests. This build decodes the variants it knows (read,
ls, grep, write, delete, shell, fetch, diagnostics, …), rejects each one with a
message telling the model to use the DSH tools instead, and forwards MCP calls
(mcp_args) as real DSH tool calls. Cursor adds exec variants without notice:
this cycle it sent an exec whose args field is 36 — a message carrying only
a workspace name, for a tool this build has no name for.
decodeExecServerMessage returned { case: "unknown" }, rejectionFor()
returned undefined for it, and the adapter wrote no reply at all.
ExecClientMessage numbers its result variants exactly like
ExecServerMessage numbers its exec variants, so an unnameable exec is now
answered on its own field with the generic error shape
{ error = 2 { error = 1 } }, carrying the same reason the other native tools
get ("Tool not available in this environment. Use the MCP tools provided
instead."). Field 19 (span_context) is a plain non-oneof field, so it is never
mistaken for the exec; a message that carries only ids and a span context has no
exec to answer and is left alone. The adapter logs one warning per run naming the
field it declined, and a second if an exec arrives with no addressable field
(the one case that can still stall).
The stall trace names the field. decodeExecServerMessage keeps the field
number of an unknown exec and the message's top-level fields, and the stall
trace prints exec:unknown#36[1,19,36,55] instead of the bare exec:unknown
that made this hard to diagnose from the log alone.
v0.6.7 — the model list is sorted by name
Cursor's GetUsableModels answers in its own order — newest family first — and
the plugin passed that order straight through, so the DSH model picker and the
settings model list rendered 231 entries for a live account in Cursor's
sequence: Auto buried at the top, Claude Fable, then Claude Opus, then
whatever Cursor had added most recently. The order also moved between Cursor
releases, so the list reshuffled on its own.
CursorAdapter.#discoverModelssorts the list it projects from
GetUsableModels(and from the offline fallback list) before caching it, so
every surface that reads the cache inherits one order:listModelsfor the
DSH picker andlistModelsForRpcfor the settings list can no longer
disagree.- Names are compared with
Intl.Collator("en", { numeric: true, sensitivity: "base" }), and the id breaks ties:- the collation is pinned to
"en"rather than the host locale, so one build
lists the same order everywhere and the picker does not depend on the
reader's language; numerickeeps families in their natural sequence —Composer 1.5before
Composer 2,GPT-5beforeGPT-5.1beforeGPT-10— instead of
comparing the digits as text;sensitivity: "base"makes the order case-insensitive, soauto,Auto,
andGPT-4o/gpt-4oneighbours stay together;- two models that share a display name (Cursor returns several variants that
do) are ordered by id, so the list stays stable instead of depending on
response order.
- the collation is pinned to
sortModelsByName(models)is exported for callers outside the adapter. It
returns a copy, so a caller holding the cached list never has it reordered
underneath.- The hardcoded fallback list, previously written in family order, is sorted by
the same rule. - What did not change: which models are listed and their metadata — same
ids, same names, sameinputModalities, same cache lifetime (5 minutes).
resolveModelstill resolves by id, so a model selected before the upgrade
keeps working. The usage card's included-usage table still sorts model rows by
spend, the way Cursor's dashboard does; only the selectable model list is
name-ordered.
Verification
-
v0.6.9, live after the API migration:
dsh webboots with the entry
active — the loopback channel answersPOST /cursor-subscription/versionwith
Connection's401 unauthorized, which only happens onceapplyhas completed
and mounted the route. A three-step Cursor run then reportedinputTokens
17,613 / 17,544 / 18,558 andoutputTokens390 / 237 / 5: prompt sizes that
grow with the conversation, and an output count that matches the text. -
v0.6.9, compaction: in the failing session the live history after
condensing was 9,641 characters (~2,410 tokens) while the full session was
815,800 characters (~203,950 tokens, 55% of it tool output). -
v0.6.9, real-cordis boot probe: 12/12 — the entry activates against a
0.1.7-shaped settings service, the page policy is keyed by the plugin's own
fiber,Configarrives as live references, a committed edit is visible on the
next read, the settings update path writes through the entry namespace, and a
0.1.5-shaped service still loads the provider. -
v0.6.8, live reproduction: the two prompts below stalled and produced no
tool call before the fix — the step ended with only the preamble text.prompt before after read and summarise package.jsonstall, no tool call MCP readcall at 9.5sread the first paragraph of README.mdstall, no tool call MCP readcall at 11.5sSame result with
claude-opus-5-high, the model in the failing session: the
unknown exec is declined at 4.7s and the MCPreadcall arrives at 11.5s. -
**v0.6.8, reply shape probed against the live ser...
dsh-cursor-subscription v0.6.6
dsh-cursor-subscription v0.6.6
The usage card now shows what Cursor's dashboard shows. The included-usage
table it renders mirrors the dashboard's Included Usage table row for row:
one row per included pool, then the models that drew on that pool, with the
pool's token total and percentage.
Before this release the per-model row was simply wrong: the panel divided a
model's spend by the spend of the models it had listed, so a cycle with a
single active model always read 100% (and its bar filled to the end) while
the dashboard showed 15.6%.
Root cause
parseModelAggregations projected pct as model spend / total spend of the listed models. That is a share of the list, not usage of the plan allowance:
- one active model ⇒
pct = 100, whatever it actually consumed; - models missing from the list silently changed every other row's percentage;
- the row had no token count at all, so the panel could not show what the
dashboard's Tokens column shows.
Cursor's own dashboard bundle computes each row differently — a share of the
pool percentage shown directly above it:
// api rows, ef = Other Models subtotal
(ef.totalCents > 0 ? (e.totalCents / ef.totalCents * en.apiPercentUsed) : 0).toFixed(1)
// auto rows, ex = Cursor Models subtotal
(ex.totalCents > 0 ? (e.totalCents / ex.totalCents * en.autoPercentUsed) : 0).toFixed(1)Fixed
The Usage card renders Cursor's included-usage table
- An Item / Tokens / Usage table replaces the separate pool bars and the
loose per-model list. - Aggregation rows are classified the way Cursor's usage table classifies them:
tier2 (anddefaultrows with no tier) belong to Cursor Models (Auto +
Composer); every other row belongs to Other Models (API) — including
defaultrows attier1, shown as auto (overflow) because they are Auto
usage billed to the Other Models allowance. - Each pool row prints the percentage Cursor reports for that pool
(autoPercentUsed/apiPercentUsed) and the pool's token total; each model
row prints its own tokens and its share of the pool percentage, so a single
Other Models model reads exactly what the dashboard reads. - Token counts sum input, output, cache-write and cache-read tokens and are
printed compact in the reader's locale (2698.7万,27.1M); percentages
always keep one decimal, matching the dashboard's0.0%/15.6%. - Anything Cursor did not report renders as
—rather than a fabricated
number, and a failed aggregated read still shows both pool percentages from
/api/usage-summarywith the token cells unavailable. - Each model row keeps its dollar spend as a tooltip, so the Spending detail is
still one hover away.
Host projection
parseModelUsage(json, { autoPercentUsed, apiPercentUsed })now returns
{ models, pools }, where a model carries{ id, pool, spentDollars, tokens?, overflow?, pct? }and each pool carries its token total.parseModelAggregations(json, options)keeps returning just the model rows,
andusagePools(parsed, modelUsage)builds the pool facts the reader ships.- The reader emits the new
poolsfield;plan,includedRequests,
on-demand spend, and billing-cycle fields are unchanged.
Panel copy
- Added
usageItem,usageTokens,usagePercent, andautoOverflowto all
twelve languages. - Dropped the four templates the old row list needed (
autoPercent,
otherModelsPercent,otherModelsUnknown,modelSpendPct); the table
carries the pool percentage in its own column now.
Verification
-
Live: with the account that reported this (enterprise, cycle
2026-09-20 → 2026-10-20), the Host projects exactly the dashboard's rows:Item Tokens Usage Cursor Models 0 0.0% Other Models 26986544 (2698.7万) 15.6% claude-opus-5-high 26986544 (2698.7万) 15.6% usage-summaryreportedautoPercentUsed: 0,apiPercentUsed: 15.605; the
aggregated events reported
{ modelIntent: "claude-opus-5-high", tier: 1, totalCents: 3120.763055, inputTokens: "196812", outputTokens: "109116", cacheWriteTokens: "1463827", cacheReadTokens: "25216789" }. The model row read 100% before this change. -
Cross-check: the percentage formula is the one Cursor's own dashboard
chunk runs for these rows, so a model row can no longer disagree with the pool
row above it. -
Unit: 84 host-side tests pass (proto 51, client-locales 8,
client-usage-table 4, inject-contract 6, version 6, native-fetch 6,
image-input 3). New cases: a single Other Models model carries its pool
percentage (15.6, not 100), two models of one pool split it by spend
(11.7 + 3.9), Cursor-model rows scale against the auto pool while Other-model
rows scale against the API pool,tier: "2"still counts as a Cursor-model
row, an unreported pool percentage omitspct, pool token totals and the
overflow flag come out of the projection, a failed aggregated read keeps the
pool percentages, and the table groups each pool with its models in
dashboard order.
Install or upgrade
dsh plugin --profile web add dsh-cursor-subscription@0.6.6
dsh plugin --profile web list dsh-cursor-subscription --depth 0Restart DSH manually afterwards and check that Settings → Cursor shows
v0.6.6.
Compatibility and known notes
- No new endpoints: the same three dashboard reads are used
(/api/usage-summary,/api/dashboard/teams,
/api/dashboard/get-aggregated-usage-events). parseModelAggregationschanged signature (options object rather than a
positional limit) and its rows gainedpool,tokens, andoverflow; its
pctis now usage of the pool allowance rather than a share of the model
list.parseModelUsageandusagePoolsare new exports.- The reader's
modelsrows carry token counts now, and the projection gained
pools. - Cursor's Agent protocol and dashboard endpoints remain undocumented and
change over time.
dsh-cursor-subscription v0.6.6
用量卡片现在和 Cursor 官网一致:面板渲染的「包含用量」表格与官网
Included Usage 表逐行对应——每个额度池一行,其下是该池消耗过的模型,并带
上该池的 token 总量与百分比。
修复前那一行模型数据是错的:插件把某个模型的消费除以「已列出模型的消费总和」,
因此一个周期内只有一个活跃模型时永远显示 100%(进度条也顶满),而官网显示的
是 15.6%。
原因
parseModelAggregations 把 pct 投影成 模型消费 / 已列出模型消费总和。这是
“在列表里的占比”,不是套餐额度的使用率:
- 只有一个活跃模型时必然得到
pct = 100,与实际用量无关; - 列表里少一个模型,其余所有行的百分比都会被悄悄改变;
- 这一行完全没有 token 数,因此面板也无法像官网的 Tokens 列那样显示用量规模。
Cursor 官网自己的前端代码对每一行算的是另一种值——该行占它所属额度池百分比的
份额:
// Other Models(API)行,ef = 该池小计
(ef.totalCents > 0 ? (e.totalCents / ef.totalCents * en.apiPercentUsed) : 0).toFixed(1)
// Cursor Models(auto)行,ex = 该池小计
(ex.totalCents > 0 ? (e.totalCents / ex.totalCents * en.autoPercentUsed) : 0).toFixed(1)修复
用量卡片改为官网的包含用量表格
- 用一张 Item / Tokens / Usage 表格取代原来分开的两个额度池进度条和松散的模型
列表。 - 聚合行按 Cursor 用量表的同一规则归池:
tier为 2(以及没有tier的
default行)属于 Cursor Models(Auto + Composer);其余行属于
Other Models(API)——其中包括tier为 1 的default行,标注为
auto (overflow),因为它们是溢出到其他模型额度的 Auto 用量。 - 每个额度池行显示 Cursor 上报的该池百分比(
autoPercentUsed/
apiPercentUsed)与该池 token 总量;每个模型行显示自己的 token 与
“占所属池百分比”,因此只有一个 Other Models 模型时,数值与官网完全一致。 - token 数为 input、output、cache write、cache read 之和,并按读者所在区域紧凑
显示(2698.7万、27.1M);百分比固定保留一位小数,与官网的0.0%/
15.6%一致。 - Cursor 没有上报的值显示为
—,不再编造数字;聚合接口读取失败时仍会用
/api/usage-summary的两个池百分比渲染表格,token 单元格显示为不可用。 - 模型行仍把消费金额保留为鼠标悬停提示,Spending 细节依然触手可及。
Host 投影
parseModelUsage(json, { autoPercentUsed, apiPercentUsed })返回
{ models, pools }:模型为{ id, pool, spentDollars, tokens?, overflow?, pct? },每个池带自己的 token 总量。parseModelAggregations(json, options)仍只返回模型行;
usagePools(parsed, modelUsage)负责组装 Reader 下发的额度池数据。- Reader 新增
pools字段;plan、包含请求、按需消费、账单周期字段均未改动。
面板文案
- 十二种语言各新增
usageItem、usageTokens、usagePercent、autoOverflow。 - 删除旧模型列表专用的四个模板(
autoPercent、otherModelsPercent、
otherModelsUnknown、modelSpendPct);额度池百分比现在有自己的列。
验证
-
实测: 用反馈此问题的账户(enterprise,周期 2026-09-20 → 2026-10-20),
Host 投影出的行与官网逐行一致:Item Tokens Usage Cursor Models 0 0.0% Other Models 26986544(2698.7万) 15.6% claude-opus-5-high 26986544(2698.7万) 15.6% usage-summary返回autoPercentUsed: 0、apiPercentUsed: 15.605;聚合事件
返回{ modelIntent: "claude-opus-5-high", tier: 1, totalCents: 3120.763055, inputTokens: "196812", outputTokens: "109116", cacheWriteTokens: "1463827", cacheReadTokens: "25216789" }。修复前该模型行显示 100%。 -
交叉验证: 百分比公式与 Cursor 官网前端为这些行执行的公式一致,模型行不会
再和它上方的额度池行对不上。 -
单元测试: Host 侧 84 个测试全部通过(proto 51、client-locales 8、
client-usage-table 4、inject-contract 6、version 6、native-fetch 6、
image-input 3)。新增用例覆盖:单个 Other Models 模型显示其池百分比(15.6 而非
100)、同一池内两个模型按消费拆分(11.7 + 3.9)、Cursor 模型行按 auto 池缩放而
Other 模型行按 API 池缩放、tier: "2"仍归入 Cursor 模型行、池百分比缺失时
pct缺省、池 token 总量与 overflow 标记、聚合读取失败时保留池百分比,以及表格
按官网顺序把模型归到对应池下。
安装或升级
dsh plugin --profile web add dsh-cursor-subscription@0.6.6
dsh plugin --profile web list dsh-cursor-subscription --depth 0之后手动重启 DSH,确认 设置 → Cursor 显示 v0.6.6。
兼容性与已知事项
- 没有新增接口:仍然只读取同样的三个官网接口
(/api/usage-summary、/api/dashboard/teams、
/api/dashboard/get-aggregated-usage-events)。 parseModelAggregations签名有变化(由位置参数limit改为选项对象),返回行
新增pool、tokens、overflow;pct语义由“占模型列表的比例”改为“占所属
额度池额度的用量”。新增导出parseModelUsage、usagePools。- Reader 的
models行现在带 token 数,投影新增pools。 - Cursor 的 Agent 协议与官网接口均未公开,且可能随时变化。
dsh-cursor-subscription v0.6.5
dsh-cursor-subscription v0.6.5
The frequent timeouts are fixed. They were not a network problem: the trace
added in 0.6.4 shows the connection healthy and the adapter waiting for a
checkpoint that Cursor does not always send.
Root cause
A tool-call step ended like this (10 of 10 traces, 2026-09-18 16:38Z through
2026-09-19 14:46Z, claude-opus-5-high):
… tokenDelta × n | toolCallStarted | checkpoint(NB) | exec:mcpArgs | heartbeat ×6-7
Each report also carried textChars=0 toolCallPending=true,
lastFrame=1-9s ago, lastContent=63-74s ago.
lastFrame=1-9smeans the server was still sending heartbeats every ~5 s, so
the HTTP/2 connection and the network were fine.- The checkpoint arrives before the
mcpArgsburst. The adapter assumed the
opposite — its loop only left the tool-call step when a checkpoint followed
mcpArgs— so it kept reading for a frame that was never coming. After 60 s
the progress watchdog aborted the run with a retryableTIMEOUT, and DSH
discarded the step and re-ran it, up to the retry limit.
This is why the timeouts looked random: they depend on the order in which the
server happens to emit the checkpoint and the tool call.
Fixed
A tool-call step no longer waits for a checkpoint that never comes
- The wait after
mcpArgsis now capped byTOOL_CALL_SETTLE_MS(500 ms).
Live probing shows sibling calls of one burst arrive ~100 ms apart and the
closing checkpoint ~300 ms after the last call, so parallel calls still stay
in the same step, and a shape that never sends the checkpoint settles instead
of hanging. - To stop early,
ConnectFrameReadergainedpause()/resume(): a paused
reader keeps buffered frames, stops blocking, and accepts frames again when
the next DSH step continues the same Run. Ending the reader (finish()) would
have made the resumed step see instant EOF. - A stall that happens after the step has already delivered something visible —
answer text or a tool call — now stops reading instead of raising the
retryable error, so DSH never discards work the user has already seen. A run
that stalls before producing anything still fails withTIMEOUT.
The stall trace from 0.6.4 stays in place: it writes only when a stall happens,
and it is what turned this from a guess into a diagnosis.
Verification
- Live: the parallel tool-call path still emits both calls in one step
(blocks=2,finish: tool-calls, checkpoint at +287 ms, inside the settle
window) with no added latency. - Unit: 74 host-side tests pass (proto 45, client-locales 8, inject-contract
6, version 6, native-fetch 6, image-input 3). The new case replays the traced
shape — checkpoint,mcpArgs, then heartbeats only — and asserts the step
returnstool-callswith the run left resumable, and that resuming it sends
exactly one MCP result. Without the settle timer the same case takes the
5 s watchdog path.
Install or upgrade
dsh plugin --profile web add dsh-cursor-subscription@0.6.5
dsh plugin --profile web list dsh-cursor-subscription --depth 0Restart DSH manually afterwards and check that Settings → Cursor shows
v0.6.5.
Compatibility and known notes
- Stall reports keep landing in
~/.dsh/cursor-hang-trace.log; delete the file
freely, it is recreated on the next stall. - Cursor's Agent protocol remains undocumented and changes over time.
dsh-cursor-subscription v0.6.5
"经常超时"已修复。原因不是网络:0.6.4 加入的 trace 显示连接一直健康,是适配器
在等一个 Cursor 并不总会发的 checkpoint。
根因
一个工具调用步的结尾长这样(10 份 trace、10 份都一样,时间跨度 2026-09-18 16:38Z
到 2026-09-19 14:46Z,模型 claude-opus-5-high):
… tokenDelta × n | toolCallStarted | checkpoint(NB) | exec:mcpArgs | heartbeat ×6-7
每份报告还带着 textChars=0 toolCallPending=true、lastFrame=1-9s ago、
lastContent=63-74s ago。
lastFrame=1-9s说明服务端仍在每 ~5 秒发心跳,HTTP/2 连接与网络都没问题。- checkpoint 是在
mcpArgs之前到的。适配器假设的恰好相反 —— 它的读循环只在
mcpArgs之后收到 checkpoint 时才结束工具调用步 —— 于是一直等着一个永远
不会来的帧。60 秒后进度看门狗以可重试的TIMEOUT中止该 run,DSH 便丢弃这一步
并重跑,直到重试次数用尽。
这也解释了为什么超时看起来很随机:它取决于服务端碰巧先发 checkpoint 还是先发工具调用。
修复
工具调用步不再等待一个不会到来的 checkpoint
mcpArgs之后的等待现在由TOOL_CALL_SETTLE_MS(500 毫秒)封顶。实测同一次
突发里的兄弟调用相隔约 100 毫秒、收尾的 checkpoint 在最后一次调用后约 300 毫秒
到达,因此并行调用仍留在同一步里;而那种永远不发 checkpoint 的形态则会按时收尾,
不再挂住。- 为了能提前停止等待,
ConnectFrameReader新增pause()/resume():暂停的
reader 保留已缓冲的帧、不再阻塞,并在下一个 DSH 步继续同一个 Run 时重新接收帧。
用finish()结束 reader 会让续跑的步立刻看到 EOF,所以不能那样做。 - 如果卡死发生在该步已经产出可见内容(回答文本或工具调用)之后,现在会停止读取
而不是抛出可重试错误,DSH 因此不会再丢弃用户已经看到的东西。完全没有产出的卡死
仍然报TIMEOUT。
0.6.4 的卡死 trace 保留:它只在真的卡死时写入,正是它把猜测变成了确诊。
验证
- 真实链路:并行工具调用路径仍然在一步里发出两个调用(
blocks=2、
finish: tool-calls、checkpoint 在 +287 毫秒到达,落在 settle 窗口内),没有
额外延迟。 - 单元测试:74 个宿主侧测试通过(协议 45、面板文案契约 8、通道挂载 6、
版本号 6、native fetch 6、图片输入 3)。新增用例回放了 trace 里的形态 ——
checkpoint、mcpArgs、之后只有心跳 —— 断言该步返回tool-calls、run 保持可续跑,
并且续跑时只发送一条 MCP 结果。去掉 settle 定时器后,同样的用例会走 5 秒的看门狗
路径。
安装 / 升级
dsh plugin --profile web add dsh-cursor-subscription@0.6.5
dsh plugin --profile web list dsh-cursor-subscription --depth 0之后手动重启 DSH,确认 设置 → Cursor 显示 v0.6.5。
兼容性与已知说明
- 卡死报告仍写入
~/.dsh/cursor-hang-trace.log;可以随时删除,下次卡死会重新创建。 - Cursor 的 Agent 协议仍是未公开接口且会变化。
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通常意味着需要更新插件。
dsh-cursor-subscription v0.6.2
dsh-cursor-subscription v0.6.2
Parallel Cursor MCP tool calls now resume cleanly instead of failing the turn
with Tool result not provided, and three shipped features that a bad merge
reverted on master are back.
No published version ever contained the revert. 0.6.1 on npm is unaffected;
the regression existed only in git master between the merge of PR #3
(ec5e6af) and this release.
Fixed
Parallel MCP tool calls no longer produce phantom results
- Affected: any 0.6.x build containing the PR #3 merge. When Cursor issued
more than one MCP tool call in a single turn, tool continuation failed with
Tool result not provided, or a tool call appeared with no result of its own. - Cause: three defects in the same code path.
- Every MCP exec reused one fixed DSH block index (
TOOL_BLOCK_INDEX), so
the agent loop kept only a single tool-call block while the bridge still
waited for every sibling call. - Incomplete
partialToolCallframes were emitted as tool-call blocks before
the call had an id or name, creating nameless blocks that competed with the
realmcpArgsframes. - Empty or placeholder MCP execs were accepted as pending bridge entries, so
the resume path kept waiting on calls that could never complete.
- Every MCP exec reused one fixed DSH block index (
- Fix: partial tool calls are now buffered and never emitted on their own;
each valid call gets its own block index (TOOL_BLOCK_INDEX + pendingExecs.length)
so parallel calls stay distinct; an empty or unknown MCP exec is answered
immediately with anMcpErrorinstead of becoming a pending entry; and the
tool name is resolved against the run's text tools with the arguments
normalized before the block is emitted.
Features restored after the PR #3 merge reverted them
PR #3 was authored from a tree older than 0.5.8, so merging it deleted later
work. v0.6.2 restores all of it while keeping the MCP fix:
- The account-channel mount fix is back. The channel is mounted again
through a registry owner Context that declareswebServer, with
connection.rpc.handlekept as a fallback for a Connection plugin that
declareswebServeritself. Without this, DSH 0.1.5-rc.1 fails the entire
plugin tree with
cannot get property "webServer" without inject. If you build frommaster
atec5e6af, this is the failure you would hit. - The panel's version label is back. The
versionendpoint and the exported
VERSIONwere deleted whilelib/client.jskept requesting them, which
silently dropped thev0.6.xchip next to the Settings → Cursor title. - Image input is back.
prepareCursorImages, the durable-attachment
resolver, theimageinput modality, and the conversation-history image
frames were deleted, so every model advertised text only.
Changed
- Tests: 71 in total — 42 agent-protocol, 8 panel-language contract,
6 channel mount, 6 version label, 6 native fetch, 3 image input. The
agent-protocol suite gained coverage for the parallel MCP path: valid calls
get distinct block indexes, empty execs are rejected, and no phantom call
survives the resume. - Provenance: the MCP fix comes from PR #3 by
@dengzhilong; the reverts that merge carried
are undone inadde4a5.
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.5.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.2
# 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.2next to the title. cursor-subscriptionappears in the model picker, and sign-in, usage and
model cards work as before.- A turn that triggers several MCP tool calls at once completes without
Tool result not provided.
Compatibility and known notes
- Restoring the channel mount matters on DSH 0.1.5-rc.1, where
@deepseek-ai/dsh-client-connectiondeclarescredentialsalone. On
0.1.0-rc.6 theauthority: "loopback"registry option still exists. - The account channel reuses Connection's envelope, trust fence, and browser
authentication, so the browser half needed no change. - Image input requires the durable attachment service; without it the request
fails asUNSUPPORTED_CONTENTrather than silently dropping the image. - 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.2
并行的 Cursor MCP 工具调用现在能正常续跑,不再以 Tool result not provided
结束整轮;同时,一次错误合并从 master 上抹掉的三项已发布功能全部回归。
没有任何已发布版本受影响。 npm 上的 0.6.1 是好的;那次回退只存在于 git
master 上 PR #3 合并(ec5e6af)之后到本版本之前的区间里。
修复
并行 MCP 工具调用不再产生幽灵结果
- 影响范围:任何包含 PR #3 合并的 0.6.x 构建。当 Cursor 在一轮里发出多个
MCP 工具调用时,工具续跑会以Tool result not provided失败,或者出现一个
没有对应结果的工具调用。 - 根因:同一条代码路径上的三个缺陷。
- 所有 MCP exec 共用一个固定的 DSH block index(
TOOL_BLOCK_INDEX),于是
agent loop 只保留了一个工具调用块,而 bridge 仍在等待每一个兄弟调用。 - 尚未完整的
partialToolCall帧在拿到 id 与名字之前就被当成工具调用块发出,
产生了与真正的mcpArgs帧竞争的匿名块。 - 空的/占位的 MCP exec 会被登记为 pending bridge 条目,于是续跑路径一直等着
永远不可能完成的调用。
- 所有 MCP exec 共用一个固定的 DSH block index(
- 修法:partial 工具调用只缓冲、不再单独发出;每个有效调用拿到自己的
block index(TOOL_BLOCK_INDEX + pendingExecs.length),并行调用之间不再互相
覆盖;空或未知的 MCP exec 立刻回一个McpError,而不是变成 pending 条目;
工具名先在本轮可用的文本工具里解析、参数归一化后再发出该块。
恢复被 PR #3 合并回退的功能
PR #3 是从早于 0.5.8 的树上做出来的,合并它等于删掉了后续的工作。v0.6.2 把这些
全部恢复,同时保留 MCP 修复:
- 账户通道挂载修复回归。 通道重新通过一个声明了
webServer的注册 owner
Context 挂载,并对自行声明webServer的 Connection 版本保留
connection.rpc.handle回退。缺了它,DSH 0.1.5-rc.1 会让整棵插件树加载失败:
cannot get property "webServer" without inject。如果你从ec5e6af的
master构建,命中的就是这个错误。 - 面板版本号回归。
version端点与导出的VERSION被删除,而
lib/client.js仍在请求它们,于是 设置 → Cursor 标题旁的v0.6.x
徽标静默消失。 - 图片输入回归。
prepareCursorImages、持久化附件解析器、image输入模态
以及会话历史里的图片帧都被删除,导致所有模型只声明文本能力。
其他变更
- 测试:共 71 个——协议 42、面板文案契约 8、通道挂载 6、版本号 6、
native fetch 6、图片输入 3。协议套件新增了并行 MCP 路径的覆盖:有效调用拿到
互不相同的 block index、空 exec 被拒绝、续跑后不残留幽灵调用。 - 来源:MCP 修复来自 @dengzhilong 的 PR #3;
该合并带进来的回退在adde4a5中撤销。
安装前必读:pnpm 的 24 小时发布年龄门限
pnpm 11 及以后内置了 24 小时的 minimumReleaseAge 供应链防护:形如 ^0.5.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.2
# 或者先加上前面的按包名豁免,再用普通命令
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.2; - 模型选择器里出现
cursor-subscription,登录、用量与模型卡片照旧可用; - 一轮里触发多个 MCP 工具调用时能正常完成,不再报
Tool result not provided。
兼容性与已知说明
- 恢复通道挂载在 DSH 0.1.5-rc.1 上尤为关键:该版本的
@deepseek-ai/dsh-client-connection只声明credentials。在 0.1.0-rc.6 上,
注册选项authority: "loopback"仍然存在。 - 账户通道复用 Connection 的信封、信任栅栏与浏览器鉴权,因此浏览器侧无需改动。
- 图片输入依赖持久化附件服务;没有该服务时请求会以
UNSUPPORTED_CONTENT
失败,而不是静默丢弃图片。 - Cursor 的 Agent 协议仍是未公开接口且会变化;聊天链路上的传输错误或
CURSOR_ERROR通常意味着需要更新插件。
dsh-cursor-subscription v0.6.1
English | 中文
The Cursor settings panel now reads in twelve languages, labels itself with the
build that is actually serving it, and starts cleanly on DSH 0.1.5-rc.1.
Upgrade is required if DSH refuses to start with
dsh: plugin tree failed to load: failed to apply loader entry cursor-subscription (dsh-cursor-subscription): cannot get property "webServer" without inject —
that failure is what this release fixes.
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.5.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 — for this package's recent history,
the earliest published version rather than the one you just shipped. 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.
Fixed
Startup failure on DSH 0.1.5-rc.1 — cannot get property "webServer" without inject
- Affected: any 0.5.x build that mounted its account channel through
connection.rpc.handle, on a DSH whose Connection plugin no longer declares
webServer(0.1.5-rc.1). The failure took down the entire plugin tree, so DSH
did not start at all. - Cause:
HostConnectionService.registerreadsowner.webServer, and
rpc.handlebindsownerto the Context that reads the Connection service,
whosewebServerresolves through the providing plugin's own scope.
@deepseek-ai/dsh-client-connection0.1.5-rc.1 declarescredentialsalone
and mounts its own/apitransport from a nestedwebServerContext, so that
read throws for every out-of-tree consumer. DeclaringwebServerin this
plugin's owninjectcannot help — the failing read never happens on this
plugin's Context. - Fix: mount the channel by calling the same registry method Connection uses
internally, with an explicit owner Context that declareswebServer; the
publicrpc.handlestays as a fallback for a Connection plugin that declares
webServeritself. The channel path, request envelope, Host/Origin trust
fence, and browser authentication are unchanged, so the browser half needed no
change at all.
Added
Twelve panel languages
The panel carried Simplified Chinese and English; it now also ships Traditional
Chinese, Japanese, Korean, Spanish, French, German, Italian, Brazilian
Portuguese, Russian, and Arabic.
| Locale | Label | Falls back to |
|---|---|---|
zh-Hant |
繁體中文 | zh |
ja |
日本語 | en |
ko |
한국어 | en |
es |
Español | en |
fr |
Français | en |
de |
Deutsch | en |
it |
Italiano | en |
pt-BR |
Português (Brasil) | en |
ru |
Русский | en |
ar |
العربية | en |
- Each contributed language is registered into DSH's shared locale catalog, so it
appears in Settings → General → Language; everything outside the panel
falls back to English. - Arabic mirrors the panel: layout direction, progress bars, and row
alignment flip, while numeric inputs and model identifiers stay
left-to-right. The marker and the stylesheet rules are scoped to this panel,
so the rest of the shell is untouched. - Languages are contributed only where the DSH locale runtime supports language
packs (addLanguage). On an older runtime the panel keeps working in zh/en
instead of failing.
Version label in the panel header
The section title now carries the version of the installed build (for example
v0.6.1). The number comes from the plugin's own manifest over the existing
account channel, so it is never duplicated into the browser bundle and cannot
drift from the published package. A failed lookup simply omits the label — it
never turns the panel into an error state.
Changed
- Copy is now test-enforced. Every dictionary is checked against the English
key set, its{placeholders}, and brace balance; the panel is checked for keys
it reads but never defines and keys it defines but never reads. That check
found four entries no component ever read (usageError,spendUsed,
planPercent,modelsEmpty); they were removed rather than translated ten
more times. - Lockfile refreshed.
pnpm-lock.yamlstill recorded the pre-widening
0.1.0-rc.6specifier for@deepseek-ai/dsh-client-connectionwhile
package.jsondeclares>=0.1.0-rc.6 <0.2.0, which a frozen install refuses.
All 14 peer specifiers now match the manifest. - Documentation. Both readmes and AGENTS.md document the pnpm
release-age gate, the contributed languages, and the version label. - Tests: 70 in total — 41 agent-protocol, 8 panel-language contract,
6 channel mount, 6 version label, 6 native fetch, 3 image input.
Install or upgrade
# exact version (works regardless of the 24-hour gate)
dsh plugin --profile web add dsh-cursor-subscription@0.6.1
# 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.1next to the title. - Settings → General → Language lists the new languages; Arabic mirrors the
panel. cursor-subscriptionappears in the model picker, and sign-in plus usage and
model cards work as before.
Compatibility and known notes
- The account channel reuses Connection's envelope, trust fence, and browser
authentication. On@deepseek-ai/dsh-client-connection0.1.5-rc.1 the
authority: "loopback"registry option no longer exists (it is honored on
0.1.0-rc.6), so the channel inherits the same/apiprotections: loopback or
declared trusted Hosts plus a signed browser session. - Contributed panel languages need a DSH locale runtime with language-pack
support; the panel degrades to Chinese/English elsewhere without an error. - Arabic mirroring is scoped to this panel; DSH's own shell strings remain
English for contributed languages, which is what theenfallback provides. - 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.
Cursor 设置面板现在支持十二种语言、在标题旁标出正在服务你的构建版本,并修复了 DSH
0.1.5-rc.1 上的启动失败。
如果你遇到下面这个启动错误,本次升级是必须的:
dsh: plugin tree failed to load: failed to apply loader entry cursor-subscription (dsh-cursor-subscription): cannot get property "webServer" without inject
——这正是本版本修复的问题。
安装前必读:pnpm 的 24 小时发布年龄门限
pnpm 11 及以后内置了 24 小时的 minimumReleaseAge 供应链防护:形如 ^0.5.0
的版本范围永远不会选中最近 24 小时内发布的版本。所以在刚发布完之后执行
dsh plugin --profile web add dsh-cursor-subscription,解析到的是「发布超过 24 小时的
最新版本」——以本包近期的发布节奏,就会退化成最早的已发布版本,而不是你刚发布的那一个。
解决方式二选一:安装时显式指定版本,或在 profile 的 pnpm-workspace.yaml
里按包名豁免:
minimumReleaseAgeExclude:
- dsh-cursor-subscription按包名豁免只对这个包放行,其他依赖仍受该防护约束。完整的安装与验收流程见
AGENTS.md。
修复
DSH 0.1.5-rc.1 启动失败 —— cannot get property "webServer" without inject
- 影响范围:凡是把账户通道挂在
connection.rpc.handle上的 0.5.x 版本,遇到
Connection 插件不再自行声明webServer的 DSH(0.1.5-rc.1)时,整棵插件树加载
失败,DSH 完全无法启动。 - 根因:
HostConnectionService.register会读取owner.webServer,而
rpc.handle把owner绑定到读取 Connection 服务的那个 Context 上,该
Context 的webServer是沿着提供方插件自己的作用域解析的。
@deepseek-ai/dsh-client-connection0.1.5-rc.1 只声明了credentials,它自己的
/api也是用嵌套的webServerContext 挂载的,于是这次读取对任何外部插件都会抛错。
把webServer加进本插件自己的inject也无济于事——失败的读取根本不发生在本
插件的 Context 上。 - 修法:改用 Connection 内部使用的同一个注册方法,并显式传入一个声明了
webServer的 owner Context;对自行声明webServer的 Connection 版本仍保留
公开的rpc.handle作为回退。通道路径、请求信封、Host/Origin 信任栅栏与浏览器
鉴权全部不变,因此浏览器侧一行都不用改。
新增
面板支持十二种语言
此前面板只有简体中文与英文,现在新增繁体中文、日语、韩语、西班牙语、法语、德语、
意大利语、巴西葡萄牙语、俄语和阿拉伯语。
| 语言 | 显示名 | 回退到 |
|---|---|---|
zh-Hant |
繁體中文 | zh |
ja |
日本語 | en |
ko |
한국어 | en |
es |
Español | en |
fr |
Français | en |
de |
Deutsch | en |
it |
Italiano | en |
pt-BR |
Português (Brasil) | en |
ru |
Русский | en |
ar |
العربية | en |
- 新增语言会注册进 DSH 的共享语言目录,出现在 设置 → 通用 → 语言
中;面板之外的其他界面回退到英文。 - 阿拉伯语自动镜像:布局方向、进度条与行对齐翻转,数字输入框和模型名保持
从左到右。标记与样式规则都限定在本面板内,不影响 DSH 其余界面。 - 语言包只在支持该能力的 DSH locale runtime(
addLanguage)上生效;在更旧的
runtime 上面板安静地停留在中/英文,不会报错。
面板标题旁的版本号
标题旁现在会显示当前安装构建的版本(例如 v0.6.1)。这个数字由宿主从自己的
package.json 读出、经现有账户通道返回,因此不会被复制进浏览器包里,也就
不可能和发布版本不一致。读取失败时只是不显示徽标,绝不会把面板变成错误状态。
其他变更
- 文案改由测试守住:12 份词典会逐一校验键集、
{占位符}与花括号配平;面板还会
校验「读到的键都已定义」与「已定义的键都被读到」。正是这项检查发现 4 个从未被任何
组件读取的陈旧词条(usageError、spendUsed、planPercent、modelsEmpty),
它们被直接删除,而不是再翻译十份。 - 刷新 lockfile:
pnpm-lock.yaml里@deepseek-ai/dsh-client-connection仍记着
放宽前的0.1.0-rc.6,而package.json声明的是>=0.1.0-rc.6 <0.2.0,
--frozen-lockfile会直接拒绝;现在 14 个 peer specifier 与清单完全一致。 - 文档:两份 README 与 AGENTS.md 都补充...