dsh-auto-continue-community 插件
状态:v0.3.2,已发布至 npm。 全部验证已通过:95/95 单测、代码审查、性能基准、headless 端到端、 Web 端到端(含 2026-10-30 真实 GUI 会话验证,见"部署验证"节)。
安装
dsh plugin add @quekenao/dsh-auto-continue-community
安装后插件通过自带的 cordis.patch.yml bundle 自动激活,无需手动编辑配置。
⚠️ 适用边界(部署前必读)
本插件仅在以下链路下经过验证,超出该范围不保证行为正确:
| 维度 | 适用范围 |
|---|---|
| 链路拓扑 | DSH + llama.cpp + 本地模型 |
| DSH 版本 | 0.2.1-rc.5 |
@deepseek-ai/cordis |
~4.0.5 |
@deepseek-ai/dsh-llm / dsh-web-app |
0.2.1-rc.5 |
| Node.js | >= 24(开发/测试于 v24.18.0) |
| 运行端 | 仅 Web 端(dsh web 的 waterfall) |
| 客户端 / CLI | 未适配,不在本版本支持范围内(我们不交付客户端支持) |
原因:插件依赖的 DSH 行为——llm/stream waterfall、agent/assistant-stream 事件、
agent.steer() inbox 通道、BlockAssembler 的 block-index 约定、max-tokens 时丢弃
tool-call 的 kept-filter——都是 DSH 0.2.1-rc.5 这一条版本线的特性。
更换 DSH 大版本、换用非 llama.cpp 后端、或落到客户端 / CLI 端,均超出适用范围。
机器可读的同一份信息也写在
package.json的dsh.scope字段和peerDependencies/engines中,便于自动化工具校验。
与内部版的区别
本插件是 dsh-auto-continue(自用版)的社区版副本,针对"发社区/可分发性"做了多项整改:
| 整改 | 自用版 | 社区版(本插件) |
|---|---|---|
| 调试日志路径 | 硬编码绝对路径 | 可移植:os.tmpdir()/dsh-auto-continue-community-debug.log |
| provider 门控 | 硬编码 local-vision |
可配置(patch config 或环境变量,经 schema 校验),默认 local-vision |
| 续写参数 | 硬编码常量 | 可配置(maxContinuations、timeoutMs、providers、mode),zod schema 校验 + 加载期报错 |
| 续写架构 | 仅缓冲流模式 | 双模式:loop(实时,默认)+ stream(缓冲,回退) |
| 单元测试 | 无 | test/ 下 95 个单测(纯函数 + 集成 + 故障注入) |
功能概述
当配置 provider 的本地模型流式响应因 max-tokens 被截断时,插件透明地追加模型调用,
将截断的输出补完,使 agent loop 看到完整的回复。
双模式架构(v0.3.0 新增)
插件提供两种互斥的续写模式,加载时二选一(mode 配置字段,默认 loop):
| 维度 | loop(默认,实时) |
stream(回退,缓冲) |
|---|---|---|
| token 实时显示 | ✅ 是——原始步与续写步各自独立流式推送,UI 实时显示 | ❌ 否——缓冲整段上游流,finish 只在流末到达 |
| 续写方式 | 通过 agent.steer() 注入一条新的 agent 步 |
通过 llm/stream waterfall 拦截 + 重调 + 合并 |
| UI 表现 | 两条独立的 assistant 消息(原始 + 续写),各自实时流式 | 一条合并的 assistant 消息,完成后一次性出现 |
| 依赖钩子 | agent/assistant-stream + agent.steer() + agent/dispose |
llm/stream |
| 适用场景 | 默认推荐;实时体验 | loop 模式在你的部署中异常时的保证回退 |
设计取舍:loop 模式牺牲了"合并为一条消息"的美观,换来了实时 token 显示——
这是用户的核心诉求。续写是一个独立的 agent 步(不是流拼接),因此 UI 显示两条
消息,但每条都实时流式。若某次部署中 loop 模式行为异常(如 steer() 不可用),
设置 mode: stream 即可回退到缓冲模式。
可配置参数
插件通过 cordis 的 Config(standard-schema,zod)声明配置契约:apply() 收到的是
经过校验并填好默认值的对象(cordis 在 apply 前对 patch config 块做校验)。
| 参数 | 默认值 | 约束 | patch config 键 |
环境变量* | 说明 |
|---|---|---|---|---|---|
providers |
["local-vision"] |
字符串数组,长度 ≥ 1 | providers |
DSH_AUTOCONTINUE_PROVIDERS(逗号分隔) |
仅拦截这些 provider |
maxContinuations |
5 |
整数,1–20 | maxContinuations |
DSH_AUTOCONTINUE_MAX |
单条响应链最多续写次数 |
timeoutMs |
120000 |
整数,1–3600000 | timeoutMs |
DSH_AUTOCONTINUE_TIMEOUT_MS |
单次续写超时(毫秒),超时降级回原始截断流 |
mode |
"loop" |
枚举:"loop" | "stream" |
mode |
DSH_AUTOCONTINUE_MODE |
续写模式:loop(实时)或 stream(缓冲回退) |
* 环境变量是 resolveConfig 的回退路径:仅当某字段既不在 patch config、也不在
schema 默认值中时才生效。schema 默认值已覆盖四个字段,因此正常注册下环境变量通常不触发。
校验行为(这是与"硬编码常量"版的实质区别):
- 不写
config块 → cordis 传undefined给 schema,object-level.default()将其解析为默认值,等价于上面"默认值"列。 - 写了合法
config块 → 校验通过,值生效。 - 写了非法
config块(如maxContinuations: 999、providers: []、timeoutMs: 0、mode: "bogus")→ cordis 在加载阶段抛ValidationError,启动即报错,而不是 "配置被静默丢弃、插件没生效"。配置拼写错误从此大声失败。
实现细节:
Configschema 使用z.object({...}).default({...})(object-level default)。 字段级.default()只填充对象输入中缺失的字段,对undefined输入无效——而cordis.patch.yml不写config块时 cordis 恰好传undefined。object-level.default()确保undefined输入被解析为默认值而非校验失败(v0.3.1 Bug #5 修复)。
配置示例
在 cordis.patch.yml 中,config 是 insert 条目上的一个字段(loader 把条目上的
config 透传给插件,与 id/name 同级):
- insert:
- id: auto-continue-community
name: "@quekenao/dsh-auto-continue-community"
config:
providers:
- local-vision
- llama-local
maxContinuations: 3
timeoutMs: 60000
mode: loop # 'loop'(默认)或 'stream'
这个 config 块会经过上面声明的 Config schema 校验:合法则生效,非法则加载时报
ValidationError。
或纯环境变量(不改 yml,走 resolveConfig 回退路径):
set "DSH_AUTOCONTINUE_PROVIDERS=local-vision,llama-local"
set "DSH_AUTOCONTINUE_MAX=3"
set "DSH_AUTOCONTINUE_TIMEOUT_MS=60000"
set "DSH_AUTOCONTINUE_MODE=stream"
工作流程
loop 模式(默认,实时)
agent loop 运行一步(runtime.stream 内部,实时流式推送 UI)
│
▼
agent/assistant-stream emit 事件(逐帧)
│
├─ 首帧(type='start')→ markLoopPending(agent)(标记流在途)
│
├─ finish 帧(type='chunk', chunk.type='finish')
│ → recordLoopFinish(agent, chunk.reason.kind)
│ ├─ kind === "max-tokens" → state.truncated = true
│ └─ 其他 → state.truncated = false, state.count = 0(链结束)
│
├─ max-tokens 且 continuationDecision → continue:
│ │
│ ├─ providerOk(agent)?
│ │ (1) 插件 meta.provider ∈ providers → 放行
│ │ (2) agent.provider ∈ providers → 放行
│ │ (3) 无法确定 provider → fail-open 放行
│ │
│ ├─ 最后一条 user message 带 CONTINUATION_MARK → 跳过(重入守卫)
│ │
│ ├─ state.count ≥ maxContinuations → 跳过(预算耗尽)
│ │
│ └─ 构建续写消息 + agent.steer(continuationMsg)
│ │
│ ├─ 有已完成 tool call → 用 CONTINUE_PROMPT_TEXT_WITH_TOOLCALLS
│ │ (告知模型 tool call 未执行、不在上下文中、不要重新发出)
│ └─ 无 tool call → 用 CONTINUE_PROMPT_TEXT
│ │
│ ▼
│ steer() → inbox.nextStep + wakeDriver
│ │
│ ▼
│ agent loop 的 turn() 检测到 inbox.nextStep 非空
│ → 执行新一步(续写步),实时流式推送 UI
│
└─ 如果续写也 max-tokens → stream observer 再次检测到
→ 再次 steer()(受预算约束)
如果续写 stop / tool-calls → 链结束
关键:
- 续写是独立的 agent 步,不是流拼接。UI 显示两条 assistant 消息,各自实时流式。
- 注入通道是
agent.steer()(不是agent/pre-step):pre-step在 step 开始前触发, 而 max-tokens 检测发生在流结束时刻,此时 turn 已关闭、pre-step不再触发。steer()将消息追加到inbox.nextStep并唤醒 driver,turn()的循环在inbox.nextStep非空时保持 turn 打开,续写步立即执行。 agent/dispose事件清理该 agent 的 loop 状态,防止泄漏。
stream 模式(回退,缓冲)
agent loop 调用 runtime.stream(options)
│
▼
llm/stream waterfall 触发
│
├─ provider ∉ providers → 透传(next())
│
├─ 缓冲上游流(collectChunks)
│ │
│ ▼
│ analyzeChunks: 检测 finish.kind === "max-tokens"
│ │
│ ├─ 未截断 → resetCounter,原样 yield
│ │
│ ├─ 截断 + 有未完成 tool call → 跳过续写,原样 yield
│ │ (JSON 参数被截断,无法修复;DSH 会丢弃所有 tool-call block)
│ │
│ └─ 截断(可续写)→ 检查预算
│ │
│ ├─ 预算耗尽 → 原样 yield
│ │
│ └─ 有预算 → 构建续写 prompt
│ │
│ ├─ 有已完成 tool call → CONTINUE_PROMPT_TEXT_WITH_TOOLCALLS
│ └─ 无 tool call → CONTINUE_PROMPT_TEXT
│ │
│ ▼
│ runtime.stream(continuationOptions)
│ │
│ ├─ 成功 → mergeChunks(合并两段流)
│ │ │
│ │ ├─ block index 偏移:continuation 的 block 加 offset,避免碰撞
│ │ ├─ usage 合并:completion_tokens 求和,prompt_tokens 取 continuation
│ │ ├─ replayState 剥离:避免 BlockAssembler 长度检查失败
│ │ └─ 终端 finish:优先 continuation 的 finish
│ │
│ ├─ 超时 → abort,yield 原始截断流
│ └─ 错误 → yield 原始截断流(降级)
│ │
│ ▼
│ 如果续写未截断 → resetCounter
│ 如果续写也截断 → 保留计数(下次调用检查预算)
合并流结构(stream 模式)
[upstream body chunks (minus finish)]
[continuation body chunks (block indices + offset, minus finish)]
[merged usage chunk]
[terminal finish chunk (replayState stripped)]
BlockAssembler 消费顺序:block-start / delta / block-end 按 index 组装, usage 最后写入,finish 最后写入。合并结构与此一致。
计数器生命周期
loop 模式(loopState Map,per agent):
- 每步流结束 →
recordLoopFinish(设置 truncated 标志) - 未截断步结束 → count 重置为 0(链结束)
- 截断 + 注入续写 →
consumeContinuation(count+1) - 预算耗尽(count ≥ maxContinuations)→ 不注入
- agent 销毁 →
clearLoopState(清理) - LRU 清理:Map 超 256 条且距上次清理 ≥60s → 淘汰最旧的,保留 128 条
stream 模式(sessionCounters Map,per session):
- 每次非截断响应 →
resetCounter(重置为 0) - 每次续写 →
incCounter(+1) - 续写也截断 → 不重置(保留计数,下次调用检查预算)
- 预算耗尽(≥ maxContinuations)→ 透传,不再续写
- LRU 清理:同上
重入守卫
续写请求的 messages 最后一条 user message 带有枚举字符串键属性 CONTINUATION_MARK
('__dsh_auto_continue_continuation__',值为 true)。两种模式都检测该标记:
- loop 模式:
continuationDecision检查lastMsg[CONTINUATION_MARK],有标记则不二次注入。 - stream 模式:waterfall 检测到标记时直接透传,不二次缓冲、不二次续写。
为什么是 enumerable 字符串键? DSH session 层的 snapshotJsonValue 使用
Reflect.ownKeys + propertyIsEnumerable 遍历对象——任何含非枚举 own property
的对象都会被拒绝("non-JSON-serializable data")。续写消息会经 agent.steer()
进入 inbox 并被 session.append() 序列化,因此标记必须是 enumerable 的。
字符串键在 JSON.stringify 中可见(session 日志中可见 __dsh_auto_continue_continuation__: true),
但不在消息的 content 数组内,对 LLM 不可见。
降级策略
loop 模式:续写步本身失败(模型错误)时,agent loop 会显示错误,用户可手动重试。
续写步的 max-tokens 由 stream observer 再次检测(受预算约束),通过 steer() 注入下一步。
stream 模式:续写流失败(超时、网络错误、模型错误)时,插件 yield 原始截断流。
agent loop 看到 finish.kind === "max-tokens",UI 显示"已达到输出 token 上限"提示,
用户可手动发送"继续"。
模式回退:若 loop 模式在你的部署中行为异常(如 steer() 不可用),
设置 mode: stream 即可回退到缓冲模式。两种模式互斥,加载时二选一。
已知限制
loop 模式的两条消息:续写是独立的 agent 步,UI 显示两条 assistant 消息 (原始 + 续写),而不是一条合并消息。这是实时流式的代价。若需合并为一条, 使用
mode: stream(但会失去实时显示)。sticky max-tokens(cosmetic):agent-loop 的 sticky 逻辑(
turnEnds一旦设为max-tokens就不再覆盖)导致即使续写步以stop结束,turn 仍标记为max-tokens, UI 显示"已达到输出 token 上限"提示。这是 agent-loop 的行为,不是插件 bug。 实际内容已完整,提示是多余的。未完成 tool call 不续写:如果截断发生在 tool call 的 JSON 参数中间,插件跳过续写。 原因:DSH 在 max-tokens finish 时丢弃所有 tool-call block;模型没有部分 block 的上下文; 重新发出同样的 tool call 仍会被同样的 max-tokens 上限截断。
replayState 被剥离(stream 模式):合并流的 finish chunk 中
replayState设为undefined。原因:continuation 的 replay envelope 只覆盖 continuation 自己的 block, 合并后 block 总数不匹配,BlockAssembler 会静默丢弃。剥离后 agent loop 不附带 replay 到 assistant message(功能降级,不影响正常对话和 tool call 执行)。loop 模式不受此影响 (续写是独立步,有自己完整的 replay envelope)。模型"假性截断"(false stop)不会触发续写:插件只识别协议层的
finish.reason.kind === "max-tokens"。如果本地模型在输出中途自愿停止 (finish reason 为stop,但文本停在半句话、未闭合代码块),插件按设计 不注入续写——这是模型行为,不是插件 bug(2026-10-30 真实 GUI 会话 实测复现:qwen3.8:64k 在 4827 token 处输出 EOS,文本停在半句,插件正确停止)。 一句话:不输出的是模型本身(模型自愿停止),插件按设计不续写、行为正确。 识别"语义上没写完"需要启发式(如未闭合代码块检测),属于行为变更, 不在本版本范围;v0.3.2 的 finish 日志(见"调试"节)使此类情况可被 日志直接确认,无需挖事件日志。
调试
文件日志默认关闭。 插件稳定后文件日志很少用到,且每轮对话的每次 LLM 调用都会追加几行, 长会话会持续增大(单个会话可达数百 KB),因此改为按需开启。
- 开启方式:启动
dsh web前设置环境变量DSH_AUTOCONTINUE_DEBUG=1。 未设置时插件完全不写文件(连node:fs/node:os都不加载)。 - 日志路径(可移植):
os.tmpdir()/dsh-auto-continue-community-debug.log(追加写入,所有会话共用,无轮转)。Windows 下通常是%TEMP%。 - 启动标记:开启后进程启动时写入
process started (pid ...); plugin code with file logging is active。 - 每个 step 的 finish 都有日志(v0.3.2 新增):
max-tokens→step ended max-tokens (continuation candidate)(+ 注入/跳过日志);- 其他 kind(
stop/tool-calls/error/aborted/ 空)→step ended <kind> (not max-tokens; chain ends, no continuation)。 这样"为什么这条响应链停在第 N 次续写、没有第 N+1 次"可以直接从 debug 日志回答 (典型场景:模型在stop处自愿结束,插件按设计不续写),无需再挖.zstd会话事件日志里的 finish chunk。
- 无论开关是否开启,所有日志都写入
ctx.logger.info(cordis 内存日志); 文件日志只是额外的一份落盘副本。 - 不写
process.stdout(避免 GUI 聊天视图被刷屏)。
安全模型
结论:插件本身安全,攻击面极小,不引入任何新的外部信任边界。
以下副作用面清单来自对 index.js 全量扫描(逐行核对所有可产生副作用的调用点)。
副作用面清单
| 能力 | 是否存在 | 说明 |
|---|---|---|
| 子进程 / 命令执行 | ❌ 无 | 没有 child_process / exec / spawn,不碰 shell |
| 任意网络请求 | ❌ 无 | 没有 http / fetch / net / dns;续写走 DSH 内部 runtime.stream(),不新增网络出口 |
| 动态代码执行 | ❌ 无 | 没有 eval / new Function / vm,不可被注入执行代码 |
| 任意文件读写 | ❌ 无 | 唯一的文件写是调试日志,且默认关闭 |
| 进程控制 | ❌ 无 | 没有 process.exit / abort |
| 读环境变量 | ✅ 有(只读) | DSH_AUTOCONTINUE_* 五个开关,只读不写 |
| 写调试日志 | ✅ 有(默认关) | appendFileSync 到 os.tmpdir()/dsh-auto-continue-community-debug.log |
逐条说明
唯一的"写"是调试日志,且默认关闭。 不设
DSH_AUTOCONTINUE_DEBUG=1时, 连node:fs都不加载(动态await import在开关分支内),文件系统零接触。 开启后也只往tmpdir()里追加一个固定文件名的日志: 路径是path.join(tmpdir(), 常量),不拼接任何用户输入,无目录穿越风险。续写不扩大信任边界(关键点)。 插件没有"把数据发给新地方"—— 它只是让 DSH 把原本就要调用的同一个本地 llama.cpp 再调一次 (loop 模式:
agent.steer(continuationMsg);stream 模式:runtime.stream(continuationOptions)), provider/model/signal 原样继承自原始请求。原始请求安全(本地模型),续写就安全; 它无法把数据导向云端或任意第三方。数据流是"本地 → 本地模型",没有外泄通道。 插件处理的是 DSH 内部已有的对话内容, 续写请求也回到同一个本地后端。不存在"把上下文发给外部 API"的路径。
失败时是降级,不是破坏。 loop 模式续写步失败时 agent loop 显示错误; stream 模式续写超时/出错时 yield 原始截断流。都不会损坏数据、不会卡死进程。
与解耦原则的一致性
- 未注册时:零加载、零监听、零影响。
- 注册但开关关闭时:不写文件;仅在检测到 max-tokens 截断时才续写(而那本就是它被注册的目的)。
- 自身失效时:续写异常被 try/catch 捕获并降级;cordis 的
_reload也只停用插件本身,宿主进程不受影响。
两个"非安全但相关"的注意点
- token 成本 / 资源:每次截断会多调一次本地模型,长输出最多续写
maxContinuations(默认 5)次。 是资源消耗,不是安全问题,但在受限环境要心里有数。 - provider 门控是唯一的安全闸门:插件靠"只拦截配置的 provider"来保证不误伤云端 API。
该闸门现在可配置——误把云端 provider 加进
providers,会让本应走云端优化的请求被本地续写逻辑接管。 这不是插件不安全,而是"配置错误会改变行为"。默认值为['local-vision'], 只要不改默认、且遵循上方"适用边界",就不会误伤。
部署验证(2026-10-30)
Headless 验证
使用 headless 独立进程验证(dsh --profile headless --patch <overlay>),
不重启 web GUI 或 llama 后端。Overlay 将 headless 默认 agent 路由到
local-vision(本地 llama.cpp 后端),maxTokens 强制 512。
| 检查项 | 结果 |
|---|---|
插件加载(apply() completed; mode=loop) |
✅ |
provider 路由(request/header 显示 local-vision, maxTokens 512) |
✅ |
截断检测(3 次 step ended max-tokens) |
✅ |
steer() 注入(injecting continuation 1/3, 2/3, 3/3) |
✅ |
| 续写次数 ≤ maxContinuations(恰好 3) | ✅ |
| 截断步 outputTokens(三次均 512) | ✅ |
最终步正常完成(87 tokens, stop finish) |
✅ |
| 短问题不续写 | ✅ |
Web 端到端验证
在 web surface(port 3098 并行实例,DSH_AUTOCONTINUE_DEBUG=1)上通过 web API
创建会话、发送长 prompt 验证,不重启 port 3097 主实例。agent-default-model
由 launch.patch.yml 覆盖为 local-vision/qwen3.8:64k-vision-Q5-KS,maxTokens 8192。
| 检查项 | 结果 |
|---|---|
插件加载(apply() completed; mode=loop,无 ValidationError) |
✅ |
截断检测(3 次 step ended max-tokens) |
✅ |
steer() 注入(injecting continuation 1/5, 2/5, 3/5 via steer()) |
✅ |
| 续写次数 ≤ maxContinuations(3 次,默认上限 5) | ✅ |
| 截断步 outputTokens(三次均 8192) | ✅ |
最终步正常完成(3227 tokens, stop finish) |
✅ |
| Session 结构(4 条独立 assistant 消息 + 3 条 continuation 消息) | ✅ |
短问题不续写(1 条消息,26 tokens,stop,0 continuation) |
✅ |
Provider 门控 fail-open(providerOk branch=fail-open) |
✅ |
Bug #5 发现:port 3097 主实例上截断后无续写 → 诊断发现
cordis.patch.yml无config:块时 cordis 传undefined给 zod schema →z.object拒绝undefined→ fiber INACTIVE →apply()未执行。修复:object-level.default()(v0.3.1)。 详见HANDOFF.md§4 Bug #5 和 §6c。
真实 GUI 会话验证(2026-10-30,port 3097 主实例)
在主实例(pid 7344 / 12272,DSH_AUTOCONTINUE_DEBUG=1)上用 HANDOFF §2 的
Redis 10 主题 prompt 创建了真实 Web 会话(session-56e48e06-...),并解压
session.v4.jsonl.zstd(多帧 zstd,33 个事件)逐事件核对:
| 检查项 | 结果 |
|---|---|
step 1 finish = max-tokens(8192 tokens)→ 注入续写 1/5 |
✅ 日志 + 事件日志双确认 |
step 2 finish = max-tokens(8192 tokens)→ 注入续写 2/5 |
✅ 日志 + 事件日志双确认 |
step 3 finish = stop(模型自愿 EOS,4827 tokens)→ 不注入续写(设计行为) |
✅ 事件日志 stopReason: "stop" |
| 续写次数 ≤ maxContinuations(2/5) | ✅ 预算未耗尽,链因模型 stop 正常结束 |
| debug 日志完整(appendFileSync 同步写,无缓冲丢失) | ✅ 9 行,与事件日志时间线一致 |
结论:插件逻辑健壮——两次 max-tokens 均正确续写,stop 步按设计不续写。
"2 轮后停止"的观感来自模型在 4827 token 处输出 EOS(文本停在半句,属模型
"假性截断",见"已知限制"第 5 条),不是插件故障。该验证暴露的可观测性缺口
(stop 步无日志)已在 v0.3.2 修复。详见 HANDOFF.md §6d。
单元测试
纯函数(analyzeChunks、finishKind、sumUsage、mergeChunks、
计数器 store getCounter/incCounter/resetCounter/pruneCounters、
loop 状态 store recordLoopFinish/markLoopPending/continuationDecision/
consumeContinuation/clearLoopState/pruneLoopState、resolveConfig)
从 index.js 以 named export 导出,可在隔离环境测试,无需启动 DSH 或 llama.cpp。
cd plugins/dsh-auto-continue-community
npm test # = node --test test/*.test.js
覆盖点(95 个测试):
finishKind:对象/字符串两种 finish reason 形态归一analyzeChunks:截断检测;tool-call 完整/不完整判定(含缺 index 的 delta 边界)sumUsage:null 处理、completion 求和、prompt 取 continuationmergeChunks:block index 偏移避免碰撞、usage 合并为单一 chunk- 计数器:LRU 顺序、超阈值淘汰、默认值
resolveConfig:默认值(含mode: 'loop')、patch config 数组/字符串、env 回退、优先级Configschema:默认值填充(含mode)、合法值通过、越界/空值/非法 mode 产生 issuesapply():config 走第二参(而非不存在的ctx.options.config);缺省 config 时降级不崩- loop 状态:
recordLoopFinish(max-tokens 保留 count / stop 重置 count)、markLoopPending、continuationDecision(未截断/预算耗尽/重入守卫/tool-call prompt 选择)、consumeContinuation(递增)、clearLoopState(删除)、pruneLoopState(LRU 淘汰)
文件清单
| 文件 | 说明 |
|---|---|
index.js |
插件主体(双模式;纯函数为 named export) |
package.json |
包元数据(v0.3.2;scripts.test = node --test test/*.test.js) |
cordis.patch.yml |
插件注册(含 config 块说明头注,含 mode 字段) |
test/analyze.test.js |
流分析/合并纯函数单测(12 tests) |
test/counters.test.js |
计数器 LRU + resolveConfig 单测(14 tests) |
test/config.test.js |
Config schema 校验(含 mode 字段)+ apply() 第二参配置传递回归(10 tests) |
test/loop-state.test.js |
loop 状态 store 纯函数单测(17 tests) |
test/integration.test.js |
集成测试:mock DSH 框架 waterfall,两种模式全路径(21 tests) |
test/fault-injection.test.js |
故障注入:null/undefined/畸形帧/序列化不变量/内存泄漏(21 tests) |
README.md |
本文档 |
HANDOFF.md |
交接文档(验证结果、Bug 修复记录、待办事项) |
PUBLISH-GUIDE.md |
发布指南(npm / GitHub / DSH 社区发布流程和注意事项) |
总计 95 个测试,全部通过。
修改历史
2026-07-12: 初版(自用版
dsh-auto-continue)+ 4 项审核修复 (usage 求和、block index 偏移、null-safe 日志、LRU 清理)2026-07-12: 第二项审核修复(replayState 剥离、缺 index delta 判定改进)
2026-10-30: 自用版文件调试日志改为默认关闭
2026-10-30: 社区版(本插件)从自用版复制并整改: 可移植调试日志路径、provider/参数可配置、纯函数导出 + 23 个单测。 核心算法未改动。
2026-10-30: 修复 config 传递 bug + 引入 schema 校验。 此前
apply(ctx)从不存在的ctx.options.config取值,patchconfig块被静默丢弃 ("可配置"只剩环境变量一条腿)。现改为apply(ctx, config)接收第二参,并为插件 声明 zodConfigschema:cordis 在加载阶段校验config块,合法填默认值、非法抛ValidationError。新增 8 个单测(共 31 个),package.json增zodpeerDependency。2026-10-30: v0.3.0 — 双模式架构。新增
mode配置字段(loop|stream,默认loop):- loop 模式(实时,默认):通过
agent.steer()注入续写步, 通过agent/assistant-stream事件检测 max-tokens,token 实时流式显示。 续写是独立的 agent 步(UI 显示两条消息),不是流拼接。 - stream 模式(缓冲,回退):保留原有的
llm/stream拦截 + 合并逻辑, 作为 loop 模式异常时的保证回退。 - 新增
DSH_AUTOCONTINUE_MODE环境变量。 - 新增 loop 状态 store(
loopStateMap)+ 6 个纯函数导出。 - 新增 19 个 loop 状态单测(共 50 个)。
apply()根据mode分派到registerLoopMode或registerStreamMode。
- loop 模式(实时,默认):通过
2026-10-30: 鲁棒性加固。
CONTINUATION_MARK从Symbol.for(...)改为字符串键__dsh_auto_continue_continuation__。registerLoopMode中所有agent.id访问添加 null/undefined 防护。registerStreamMode添加runtime/runtime.stream存在性检查。CONTINUATION_MARK加入 named exports。- 新增故障注入 + 集成测试(共 95 个)。
2026-10-30: P0 真实部署验证 + 4 个架构级 Bug 修复(headless 独立进程,不重启 web GUI)。
验证过程中发现并修复了 4 个导致 loop 模式完全不可用的 P0 Bug:
providerOk始终 false:ReactLoopAgent无顶层.provider/.route字段, cordis 不传 provider-bearing meta,导致 provider 门控永远拒绝。 修复:重写providerOk为三级判定——(1) 插件 meta.provider (2) agent.provider (3) fail-open(无法确定时放行)。Stream observer 读错字段:原代码从 frame envelope 读 finish reason, 实际 finish 信息在
frame.chunk.reason.kind内(AssistantStreamFrame的chunk字段)。修复:改为frame.type==='chunk' && frame.chunk.type==='finish'时读frame.chunk.reason.kind。agent/pre-step是死代码:pre-stepwaterfall 在 step 开始前触发, 而 max-tokens 检测发生在流结束时刻——此时 turn 已关闭,pre-step不再触发。 修复:删除pre-step监听器,改为在 stream observer 中调用agent.steer(continuationMsg)。steer()将消息追加到inbox.nextStep并唤醒 driver,turn()的循环在inbox.nextStep非空时保持 turn 打开, 续写步立即执行。steer()序列化失败:DSH session 层walkJsonValue使用Reflect.ownKeys+propertyIsEnumerable——任何含非枚举 own property 的对象被拒绝("non-JSON-serializable data")。 修复:CONTINUATION_MARK改为 enumerable: true;续写消息加id: randomUUID()(inbox.mutate()用message.id做去重检测)。
验证结果:headless 截断测试 3/3 续写成功(512-token 截断确认), 短问题不续写,95/95 单测通过。详见
HANDOFF.md§6。2026-10-30: v0.3.1 — Bug #5 修复 + Web 端到端验证。
Web 端到端测试(port 3098 并行实例)发现插件在 web surface 完全不激活:
cordis.patch.yml无config:块时 cordis 传undefined给 zodConfigschema,z.object({...})无 object-level.default()时拒绝undefined输入, fiber 进入 INACTIVE,apply()从未执行。修复:给
Config的z.object加 object-level.default({providers, maxContinuations, timeoutMs, mode}), 使undefined输入解析为默认值。字段级.default()对undefined无效 (只填充对象输入中缺失的字段),必须用 object-level default。验证结果:
- Web 长文续写:4 条独立 assistant 消息(1 原始 + 3 续写),
3 次 max-tokens → 3 次
injecting continuation N/5 via steer(), 最终步 3227 tokensstopfinish。 - Web 短问题不续写:1 条消息,26 tokens,
stop,0 continuation。 - 95/95 单测通过。详见
HANDOFF.md§4 Bug #5 和 §6c。
- Web 长文续写:4 条独立 assistant 消息(1 原始 + 3 续写),
3 次 max-tokens → 3 次
2026-10-30: 发布准备。新增
PUBLISH-GUIDE.md,记录 npm / GitHub / DSH 社区 发布的完整流程和注意事项。2026-11-01: npm 发布。包名改为
@quekenao/dsh-auto-continue-community, 补 LICENSE(MIT)、files白名单、publishConfig.access=public、prepublishOnly测试门禁。dsh plugin add @quekenao/dsh-auto-continue-community即可安装。2026-10-30: v0.3.2 — 真实 GUI 会话验证 + finish 日志可观测性。
- 真实 GUI 验证通过:在 port 3097 主实例上用 Redis 10 主题 prompt 创建
真实会话(
session-56e48e06-...),解压.zstd事件日志逐事件核对: step 1/2max-tokens(各 8192 tokens)→ 注入续写 1/5、2/5;step 3 模型 自愿stop(4827 tokens)→ 按设计不续写。插件逻辑健壮,"2 轮后停止" 是模型 false stop 行为,非插件 bug。 - 新增 finish 日志(纯日志,零行为变更):非
max-tokens的 step finish 也记一行step ended <kind> (not max-tokens; chain ends, no continuation)。 此前stop步无任何日志,"为什么没续写"只能靠挖.zstd事件日志定位; 现在 debug 日志(DSH_AUTOCONTINUE_DEBUG=1)可直接回答。 - 95/95 单测通过,
node --check通过。
- 真实 GUI 验证通过:在 port 3097 主实例上用 Redis 10 主题 prompt 创建
真实会话(
No comments yet. Be the first to write one.