DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

quekenao /

quekenao/dsh-auto-continue-community

Verified

Auto-continue plugin for DSH: continues model responses truncated at max-tokens

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@92c8013d

dsh-auto-continue-community 插件

GitHub repo npm version npm downloads license

状态: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,启动即报错,而不是 "配置被静默丢弃、插件没生效"。配置拼写错误从此大声失败。

实现细节:Config schema 使用 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 即可回退到缓冲模式。两种模式互斥,加载时二选一。

已知限制

  1. loop 模式的两条消息:续写是独立的 agent 步,UI 显示两条 assistant 消息 (原始 + 续写),而不是一条合并消息。这是实时流式的代价。若需合并为一条, 使用 mode: stream(但会失去实时显示)。

  2. sticky max-tokens(cosmetic):agent-loop 的 sticky 逻辑(turnEnds 一旦设为 max-tokens 就不再覆盖)导致即使续写步以 stop 结束,turn 仍标记为 max-tokens, UI 显示"已达到输出 token 上限"提示。这是 agent-loop 的行为,不是插件 bug。 实际内容已完整,提示是多余的。

  3. 未完成 tool call 不续写:如果截断发生在 tool call 的 JSON 参数中间,插件跳过续写。 原因:DSH 在 max-tokens finish 时丢弃所有 tool-call block;模型没有部分 block 的上下文; 重新发出同样的 tool call 仍会被同样的 max-tokens 上限截断。

  4. replayState 被剥离(stream 模式):合并流的 finish chunk 中 replayState 设为 undefined。原因:continuation 的 replay envelope 只覆盖 continuation 自己的 block, 合并后 block 总数不匹配,BlockAssembler 会静默丢弃。剥离后 agent loop 不附带 replay 到 assistant message(功能降级,不影响正常对话和 tool call 执行)。loop 模式不受此影响 (续写是独立步,有自己完整的 replay envelope)。

  5. 模型"假性截断"(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

逐条说明

  1. 唯一的"写"是调试日志,且默认关闭。 不设 DSH_AUTOCONTINUE_DEBUG=1 时, 连 node:fs 都不加载(动态 await import 在开关分支内),文件系统零接触。 开启后也只往 tmpdir() 里追加一个固定文件名的日志: 路径是 path.join(tmpdir(), 常量),不拼接任何用户输入,无目录穿越风险。

  2. 续写不扩大信任边界(关键点)。 插件没有"把数据发给新地方"—— 它只是让 DSH 把原本就要调用的同一个本地 llama.cpp 再调一次 (loop 模式:agent.steer(continuationMsg);stream 模式:runtime.stream(continuationOptions)), provider/model/signal 原样继承自原始请求。原始请求安全(本地模型),续写就安全; 它无法把数据导向云端或任意第三方。

  3. 数据流是"本地 → 本地模型",没有外泄通道。 插件处理的是 DSH 内部已有的对话内容, 续写请求也回到同一个本地后端。不存在"把上下文发给外部 API"的路径。

  4. 失败时是降级,不是破坏。 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 取 continuation
  • mergeChunks:block index 偏移避免碰撞、usage 合并为单一 chunk
  • 计数器:LRU 顺序、超阈值淘汰、默认值
  • resolveConfig:默认值(含 mode: 'loop')、patch config 数组/字符串、env 回退、优先级
  • Config schema:默认值填充(含 mode)、合法值通过、越界/空值/非法 mode 产生 issues
  • apply():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 取值,patch config 块被静默丢弃 ("可配置"只剩环境变量一条腿)。现改为 apply(ctx, config) 接收第二参,并为插件 声明 zod Config schema:cordis 在加载阶段校验 config 块,合法填默认值、非法抛 ValidationError。新增 8 个单测(共 31 个),package.json 增 zod peerDependency。

  • 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(loopState Map)+ 6 个纯函数导出。
    • 新增 19 个 loop 状态单测(共 50 个)。
    • apply() 根据 mode 分派到 registerLoopMode 或 registerStreamMode。
  • 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:

    1. providerOk 始终 false:ReactLoopAgent 无顶层 .provider/.route 字段, cordis 不传 provider-bearing meta,导致 provider 门控永远拒绝。 修复:重写 providerOk 为三级判定——(1) 插件 meta.provider (2) agent.provider (3) fail-open(无法确定时放行)。

    2. Stream observer 读错字段:原代码从 frame envelope 读 finish reason, 实际 finish 信息在 frame.chunk.reason.kind 内(AssistantStreamFrame 的 chunk 字段)。修复:改为 frame.type==='chunk' && frame.chunk.type==='finish' 时读 frame.chunk.reason.kind。

    3. agent/pre-step 是死代码:pre-step waterfall 在 step 开始前触发, 而 max-tokens 检测发生在流结束时刻——此时 turn 已关闭,pre-step 不再触发。 修复:删除 pre-step 监听器,改为在 stream observer 中调用 agent.steer(continuationMsg)。steer() 将消息追加到 inbox.nextStep 并唤醒 driver,turn() 的循环在 inbox.nextStep 非空时保持 turn 打开, 续写步立即执行。

    4. 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 给 zod Config schema, 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 tokens stop finish。
    • Web 短问题不续写:1 条消息,26 tokens,stop,0 continuation。
    • 95/95 单测通过。详见 HANDOFF.md §4 Bug #5 和 §6c。
  • 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/2 max-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 通过。
—/ 5

No ratings yet

Verified DSH bundle

Commit 92c8013df5ee

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout