dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
从断点继续对话:/continue 命令与消息操作行按钮让模型接着被打断的回答往下写,覆盖手动停止、模型请求失败、输出 token 上限截断与崩溃恢复四种情形,续写提示在被回答后从对话框中回收。
English | 中文
Manually resume an interrupted DeepSeek Harness conversation.
When a turn ends early — stopped by hand, cut short by a failed model request, or truncated by the output-token limit — the harness leaves nothing behind but a "stopped" marker. This plugin adds the missing step: a /continue command and a Continue action in the action row under the interrupted reply itself, so the run picks up exactly where it stopped instead of restarting.
| Situation | Without the plugin | With the plugin |
|---|---|---|
| You hit stop by accident | Retype "continue" yourself | Click Continue |
| A network or provider failure aborted the turn | Restate the context before the model resumes | The plugin hands the recorded failure to the model and resumes |
| The reply was cut off by the token limit | The harness says send "continue" — with no entry point | One click or one command continues the answer |
/continue
/continue also fix the remaining three files
Text after the command name is appended to the continuation prompt as an "Additional instruction from the user" line.
The command's outcome (已请求继续对话(中断原因:手动停止)。) is rendered by the harness command plane; it never becomes a model message.
On the turn that was cut short, a Continue action appears in the action row under the closing assistant message — the same row that carries copy / thumbs-up / thumbs-down / branch:
/continue execution.The decision comes from the session's durable event window: the newest turn/end must have a resumable reason and that turn must carry a persistent assistant message. A turn that was interrupted before producing any assistant output (a request that failed outright, a process that died before streaming) therefore has nowhere to host the button — use the /continue command there.
Type / and pick continue ("Continue the interrupted conversation from where it stopped"). Arrow keys plus Enter are enough — no draft typing required.
The plugin subscribes to the session log's session/event and tracks every turn end:
reason.kind | Meaning | Resumable |
|---|---|---|
aborted | Run cancelled. reason.reason.kind refines it: user stopped by hand, parent stopped by a parent operation, hook stopped by a harness hook, disposed session torn down | ✅ |
error | Model request failed (network, auth, server) | ✅ |
max-tokens | Reply truncated by the output-token limit (not a context-window overflow) | ✅ |
interrupted | Turn closed by crash recovery after the host process was interrupted | ✅ |
completed | Finished normally | ❌ |
blocked | Step refused by agent/pre-step before entering | ❌ |
forked | Fork seed closing an inherited prefix (the start of a new session) | ❌ |
Any later user/message clears the record — either the user took over, or the plugin already resumed the run.
/continue does not send a bare "continue". It queues a structured user message:
<continue-conversation>
The previous turn ended early: the run was stopped by hand.
Continue the interrupted work in this same session, from exactly where it stopped:
- Do not restart the task, re-summarize, or repeat output and work that is already visible above.
- Treat the current workspace, tool results, and durable session state as authoritative; …
- If a tool call was interrupted, verify its outcome before retrying it, and never blindly retry an operation that may have side effects.
- Finish the remaining work, then verify the result before reporting it.
Failure recorded by the harness: <what the harness logged when the turn died>
Additional instruction from the user: <text typed after the command>
</continue-conversation>
The first line is rewritten per reason (stopped by hand / request failed / reply cut off / crash recovery), and the harness-recorded failure text is attached so the model knows why the turn ended — which is what decides between continuing to write and re-checking an interrupted tool call first.
After you press Continue, DSH opens a new turn for the model to write on, and the plugin then retires that turn's trigger prompt from the transcript — so you no longer see the prompt, and the interrupted reply is followed directly by the new text.
The retirement is a system/message replacement, and the log makes such an event step-scoped: its turn/step must name the step that is open as it lands, and both coordinates must be positive (dsh-session-format-v3-to-v4/lib/index.js, assertV4SystemMessageFields plus the relationship replay). A turn that has already ended names no open step, so the replacement cannot be written there. It rides the step that produced the model's answer to the prompt — or, for a turn that produced none, the next step that opens.
The new text cannot be merged into the original message. That is an architectural constraint of the DSH session log, not an implementation shortcut:
replace.replace must list every shadowed node in sourceEventSeqs (dsh-session/lib/index.js:327), while assistant/message is explicitly forbidden from carrying that field (:313, it embeds its provider stream). The two rules conflict, so no assistant event can perform a replacement.system/message instead would also shadow the tool/call / tool/result pairs inside it, breaking the model history so the next request fails.The provider side has no assistant prefill either (dsh-llm-deepseek carries only anthropic-beta flags), so the model cannot literally continue the previous assistant message — it must generate a new one.
The result: no stray "continue" prompt, but still a separate new answer.
The continuation message is built by hand as { id, role: 'user', content: [{ type: 'text', text }], source: { kind: 'continue-chat' } } and frozen:
id is mandatory. Agent.inbox deduplicates pending messages by message.id (a duplicate throws already pending) and never mints one, so the plugin generates it with randomUUID() from node:crypto.source.kind is continue-chat. Chat classifies a user/message by its source kind: user would render the prompt as a human bubble, while any other kind renders it as an injected-context node — and a context node whose content is plain text stays out of the visible node set. Never use 'plugin' either: that is only the legacy format-v3 shape and format v4 rejects it when reading a log back.node:crypto: no peerDependencies, and therefore no exposure to the plugin version-compatibility gate.The client half does exactly two things:
continue-chat dictionaries (zh/en).conversation.chat.assistant-actions slot. That slot's owner prop is messageId and its entries render inside Chat's MessageIconActions — the very component that owns copy / rating / branch — so the button lands in that row by construction.Clicking it runs ctx.remote.commands.execute(sessionId, '/continue', []), the same path a hand-typed /continue takes, so command lifecycle records, result rendering, and logging all stay identical.
Whether a message belongs to an interrupted turn is answered by the session's own durable event window: ctx.sessions.binding(sessionId).eventSource is a plain getSnapshot()/subscribe() store, which the plugin reads through React.useSyncExternalStore — newest turn/end reason plus the message.id of that turn's last assistant/message. Snapshots are memoized per revision, so dozens of messages share one scan and the untouched rows never re-render.
dsh plugin --profile desktop add <absolute path to this directory>
The dsh.bundle.patch field points at cordis.patch.yml, which inserts the continue-chat row into the profile; dsh.client.platform = "web" makes the host serve lib/client.js as a browser bundle.
Remove it with:
dsh plugin --profile desktop remove dsh-continuechat
goal tool.turn/end reason, not from a guess about which turn is last./continue against a running agent reports an error instead; stop the turn first.turn/end handling./continue after a DSH restart reports "未记录中断原因" and falls back to the generic prompt — the resume still works, it just loses the reason detail.node:crypto; the client half requires only react from the platform seed table. No build step, no node_modules.node --test test/host.test.mjs test/client.test.mjs
lib/index.js — host half: interruption tracking plus the /continue command.lib/client.js — browser half: a hand-written window.__ModuleLoader__ bundle (no build step).test/host.test.mjs — drives the host half through a tiny fake cordis context.test/client.test.mjs — drives the browser bundle through a tiny fake React.tools/inspect-session.mjs — decompresses a session log (many concatenated zstd frames) and lists the /continue triggers, retirement events, and turn boundaries, for checking the surface bookkeeping: node tools/inspect-session.mjs <session dir>/session.v4.jsonl.zstd.tools/verify-retirement-coordinates.mjs — runs the harness's own released-V4 validator over the three candidate coordinates, proving that only a replacement inside an open step is accepted: node tools/verify-retirement-coordinates.mjs <app root>.MIT