Skip to content
This repository was archived by the owner on Nov 9, 2026. It is now read-only.

v0.8.9-rc.1 — 不再让不可转译的内容块打死会话 (preview)

Pre-release
Pre-release

Choose a tag to compare

@xia-sc xia-sc released this 14 Sep 01:44
· 6 commits to main since this release

v0.8.9-rc.1 — 不再让不可转译的内容块打死会话

Pre-release(预览版)。修的是 #3:
user 消息里只要出现过 harness 注解块(reasoning / tool-call),整条请求会在发出去
之前就失败;而该块已经在持久历史里,于是之后每一轮都失败、且不会自愈——用户侧表现为
「对话框完全不能用了」。

根因

  • lib/protocol.js 三端面的 user 分支都是「不认识就抛」:只认 text / image,
    其余一律 UNSUPPORTED_CONTENT。
  • 触发源不是异常输入:continuable 子代理的结算通知是 user 角色,
    createSettlementMessage 会把 terminal.output 整段展开进去,而
    finalAssistantOutput() 返回的是子会话最后一条 assistant 消息的原始 content 数组
    (含 reasoning,有时含 tool-call)。
  • 因此一条结算通知就能把历史写脏;ctx.llm.stream 每轮重放整段历史 → 每轮都在请求组装阶段抛错。
    自动救回路径也堵死:dsh-compaction-basic 的压缩请求同样是
    [...input.messages, 压缩指令],走的还是本 adapter。

修复

  • 丢弃而非抛错:user 消息只承载用户载荷(text / image),注解块与 merge-extensible
    的新类型一律丢弃。这与官方适配器一致——llm-deepseek 的 flattenText 只留 text、
    llm-pi-ai 的 userContent 走 default: break,两者都把 UNSUPPORTED_CONTENT
    留给 image。
  • assistant 侧同样处理:原来对未知类型也抛,同样会打死会话。
  • messages 端面的空 content:纯 reasoning 的 assistant 轮原本会产出 content: [],
    Anthropic 拒空数组;现在改为省略该轮(与 responses 端面既有做法一致)。
  • 空 user 轮兜底:块被全部丢弃时不再产出 content: [],而是回退成一条空 turn
    (chat 为 "",responses / messages 为一个空文本块)。
  • image 仍然硬报错:用户上传的二进制内容不能被悄悄抹掉。
  • 判定标准写进了 lib/protocol.js 顶部的块策略注释,三端面共用。

验证

  • node test/smoke.mjs:47 项,46 pass + 1 skip(无 key 的活探针)。新增 2 项回归用例,
    用 #3 里的结算通知形状断言:
    注解块被丢弃且正文保留、全丢后是空 turn 而非空数组、污染历史后三端面组装均不再抛、
    image 仍然报错。
  • npm run build:client:产物 lib/client.js 与 src/client/* 同步(无差异)。
  • 触发源确证:本机 123 个会话日志扫描,7842 条非空 assistant 消息中 4103 条带
    reasoning 块——这类消息不是边角情况。

已知 / 本版未覆盖

  • messages 端面:持久化的 tool-call arguments 不是合法 JSON 时仍抛
    PROVIDER_PROTOCOL_ERROR,属于同一类「一条历史打死整会话」的风险。它有现存测试刻意断言
    (宁可响也不降级),本版未改动,留待单独决定。
  • 「空文本块」在 responses / messages 上游是否被接受,未做真机实测(探针需要 key)。
    若上游拒收,改成「整条省略该 user 轮」是一行的事。

升级

仓库安装(link: 方式)重启即生效:

dsh plugin --profile web add github xia-sc/dsh-opencode-go
# 然后重启 dsh web,刷新页面

本地源码安装用户:拉取 main 后重启 dsh web。


v0.8.9-rc.1 — unrepresentable content blocks no longer brick a conversation

Pre-release. Fixes #3: once a user message
carried a harness annotation block (reasoning / tool-call), every request failed before it
was ever sent — and because that block is durable history, every later turn failed too, with no
self-healing. On the user's side the conversation simply stopped working.

Root cause

  • All three surfaces' user branches were "throw on anything unrecognised": only text / image
    were carried, everything else raised UNSUPPORTED_CONTENT.
  • The producer is not exotic input. A continuable subagent's settlement notice is a user message,
    and createSettlementMessage expands terminal.output into it — which is the child's raw final
    assistant content array (reasoning, and sometimes tool-call, included).
  • One settled subagent therefore poisoned the history, and ctx.llm.stream replays the whole
    history every turn, so the failure repeated at request-assembly time forever. The automatic
    rescue is blocked too: dsh-compaction-basic sends [...input.messages, instruction] through
    the same adapter.

Fix

  • Drop instead of throw: a user message carries user payload (text / image) only;
    annotation blocks and merge-extensible additions are dropped. This matches the official
    adapters — llm-deepseek's flattenText keeps text only and llm-pi-ai's userContent
    falls through via default: break; both reserve UNSUPPORTED_CONTENT for images.
  • Assistant branches too: unknown types used to throw there as well, with the same
    conversation-killing effect.
  • Empty content on the messages surface: a reasoning-only assistant turn used to emit
    content: [], which the Messages API rejects; the turn is now omitted, matching what the
    responses surface already did.
  • Empty-turn fallback: a user turn whose blocks were all dropped becomes one empty turn
    ("" on chat, one empty text block on responses / messages) rather than content: [].
  • Images still fail loudly: uploaded binary content must not vanish silently.
  • The resulting policy is documented at the top of lib/protocol.js and shared by all surfaces.

Verified

  • node test/smoke.mjs: 47 tests, 46 pass + 1 skip (live probe needs a key). Two new regression
    tests assert, using the settlement-notice shape from
    #3: annotations are dropped while the
    payload survives, an all-dropped turn becomes an empty turn rather than an empty array, poisoned
    history no longer throws on any surface, and images still fail.
  • npm run build:client: lib/client.js is in sync with src/client/* (no diff).
  • Producer confirmed: scanning 123 local session logs, 4103 of 7842 non-empty assistant
    messages carry a reasoning block — this input is the norm, not a corner case.

Known / not covered here

  • Messages surface: stored tool-call arguments that are not valid JSON still throw
    PROVIDER_PROTOCOL_ERROR — the same "one history entry kills the conversation" class. It is
    covered by an existing test that deliberately asserts the throw, so this release leaves it alone
    pending a separate decision.
  • Whether responses / messages upstreams accept an empty text block was not verified live (the
    probe needs a key). If they reject it, omitting the whole user turn instead is a one-line change.

Upgrade

Repo installs (link: style) apply on restart:

dsh plugin --profile web add github xia-sc/dsh-opencode-go
# then restart dsh web and refresh the page

Local-source installs: pull main and restart dsh web.