DeepSeek Harness Lark bridge: empty replies after format v2
Lark replies suddenly go empty while the session log clearly contains text: the bridge's projection contract is out of date, and the model and network are not involved. One commit raised SESSION_FORMAT_VERSION from 1 to 2 and deleted assistant/chunk, leaving assistant/message.message.content as the only persisted carrier of the answer. A bridge that still renders the old events and takes only usage from the message finishes the turn normally while the user receives nothing (Discussion #10074).
DeepSeek Harness session format v2: symptoms and where the body lives
The two symptoms are two stages of the same contract break, and the fact that a rollback restores replies proves the break is on the write side. The thread pins the boundary to commit f99b06eaed (feat(session)!: embed assistant streams in format v2):
- 0.1.5 generation: after upgrading from
0.1.2-rc.1to0.1.5-rc.1, Lark replies are lost silently, 100 percent of the time, while the turn is recorded ascompletedand logs and tool channels look normal. Expected check:assistant/chunkis gone from the session events. - 0.2.0 generation: on
0.2.0-rc.2the bridge handles inbound messages and tool cards, but the body is always(No content returned). Expected check:assistant/messagecontains text and the bridge projects onlyusage. - Version boundary: the commit is absent from
dsh-v0.1.2-rc.1and present fromdsh-v0.1.3-alpha.1onward;0.1.5-rc.1,0.1.7-rc.2, and0.2.0-rc.2all include it. Expected conclusion: any upgrade across that line is affected. - Rollback check: dropping back to
0.1.2-rc.1restores replies immediately. Expected conclusion: the old version still writes format 1, so the old rendering path has data to read.
Do not mix in the other problem: unreadable provider/model values in the same version family come from config resolution in the composed profile tree and are unrelated to the empty body.
After v2, core only guarantees settlement; turning that settlement into a card body is the consumer's job. The event shapes are:
assistant/messagecarries{ turn, step, message, stream, usage?, interrupted? }, wheremessage.contentholds the assembled content blocks andstreamis the timed stream that produced them.assistant/attemptcarries{ turn, step, stream }for attempts that failed, retried, were cancelled, or hit a stream error and committed no visible message.- Per-token
assistant/chunkhas been removed.agent/assistant-streamis an in-process event domain whose only remote consumer is the Web session-follow adapter, not a delivery surface for external channels. - The empty-reply diagnosis is therefore direct:
assistant/messageexists,message.contenthas text blocks, and the bridge output is empty, which means the projection dropped it.
According to the thread, the cheap official cross-check is to diff docs/persistence-catalog.md between two tags. It pins a SHA-256 for each persisted type, so any change in event shape changes the hash without requiring a source read.
DeepSeek Harness: four checks before you upgrade
Judge by the format version and the events you subscribe to, not by the bridge's own version number. Four steps run on a single machine:
- Check whether the target host tag contains commit
f99b06eaed. Expected: every tag from0.1.3-alpha.1onward contains it;0.1.2-rc.1does not. - Search the bridge source for
assistant/chunk. Expected: a hit means it still renders the old contract and will likely produce empty replies after the upgrade. - Open an existing session's event log, find
assistant/message, and inspectmessage.content. Expected: after v2 it should hold complete text blocks; if it is empty, the problem is on the model side. - Compare the bridge's
COMPATIBILITY.mdwith the SDK version it actually depends on. Expected: if the declaration covers only the old generation, apply Path 1 before upgrading the host.
Step 2 matters most: while the bridge still waits for chunks, the body will never appear no matter how complete the log is.
DSH plugin path 1: read assistant/message.message.content
This is the only way to keep the bridge moving forward with new releases, and the change is concentrated in one projection function. Steps:
- Find the session event branch in the bridge and confirm it subscribes to
assistant/chunk. Expected: you see the old logic that accumulates text per chunk. - Add handling for
assistant/message: takeevent.message.content, filter text blocks, and concatenate them in order. Expected: a complete body is available even in non-streaming runs. - Keep the
usageprojection but stop treating it as a source of body text. Expected: billing statistics stay the same and the body no longer depends onusage. - Ignore unknown event types instead of failing on them. Expected: a later format upgrade does not crash the bridge because of an added event type.
- Verify with one plain message. Expected: Lark receives the complete body and
(No content returned)disappears.
If the bridge still wants a typing effect, replay from assistant/message.stream rather than waiting for the deleted chunk events.
DeepSeek Harness path 2: roll back to 0.1.2-rc.1
This is for emergencies only, and it pins the whole host to the old generation. Order of operations:
- Confirm the SDK version the bridge depends on and which generation its compatibility statement covers. Expected: you know which host version satisfies both the bridge and your current plugins.
- Downgrade the host to
0.1.2-rc.1and restart. Expected: Lark replies return andassistant/chunkreappears in the log. - Record why and when you rolled back. Expected: the team treats this as a temporary path, not a long-term design.
- Assess the side effects: 0.2.x capabilities and any surrounding plugins that require the new generation become unavailable. Expected: the business cost of the rollback is explicit.
A rollback does not fix the bridge; it swaps the contract back. As soon as you want 0.2.x, Path 1 or Path 3 is still required.
While rolled back, do not upgrade the bridge or the SDK at the same time. Moving two variables together makes the next diagnosis harder. Keep the host version, SDK version, and bridge version on one line so a future failure shows immediately which component crossed the generation boundary.
DeepSeek Harness path 3: consume the session.event stream
The most stable posture for a channel plugin is to stop parsing session logs and consume only the SDK's stable event surface; check existing bridge plugins on DSH Plugin Hub for their compatibility claims. The minimal surface in the thread is small:
- Notifications:
session.event({ sessionId, event }),session.status(idleorrunning),subagent.started, andsubagent.finished. Expected: inbound messages, status indicators, and subagent progress are all covered. - Requests:
initialize,session/prompt,session/resume,session/wait,session/working-directory/get,session/working-directory/set, andshutdown. Expected: a complete send-and-receive loop for a chat channel. - Project
assistant/messagefromsession.eventinto the card body and drive typing state fromsession.status. Expected: no dependency on any in-process event. - Check compatibility by format version rather than client version. Expected: before upgrading, look at whether
SESSION_FORMAT_VERSIONchanged instead of waiting for users to report empty replies. - Run a real send and receive with a temporary
DSH_HOME. Expected: the body works on the new generation and nothing errors on the old one.
According to the thread, ACP is for automation and is not a channel API, so do not build a bridge on it.
DeepSeek Harness: acceptance checklist after migration
Verify four things: the body, failed attempts, unknown events, and tool blocks. Check in order:
- Send a plain text message. Expected: a complete body with no
(No content returned). - Trigger a model failure or retry. Expected: the bridge sees
assistant/attempt, does not mistake it for body text, and does not send twice. - Inject an unknown event type. Expected: the bridge ignores it and keeps working without a fatal error.
- Replay a history that contains tool calls. Expected: tool cards and body text are rendered separately and the body is not overwritten by tool blocks.
- Run the same suite against an older host. Expected: the bridge reports a version mismatch instead of silently returning an empty body.
According to the thread, this gap must be fixed in the third-party bridge's projection; there is no host-side switch to roll back. Writing "render by settlement" into the bridge's contract tests is more reliable than manually sending one message after every upgrade.
If the bridge already has a test directory, turn the four items above into cases and run them before upgrading the host. That costs far less than waiting for users to report empty replies in production. Treat event payloads as external data in those tests and do not depend on in-process state.
These four cases also work as a gate before any SDK upgrade.
Source: Discussion #10074, deepseek-ai/deepseek-harness. For other causes of a silent Lark bot, see DSH bot not replying.
FAQ
DeepSeek Harness produced output; the Lark bridge reads the wrong event. Session format v2 removed assistant/chunk, and the only persisted carrier of the answer is assistant/message.message.content. A bridge that still renders from chunks and only projects usage from the message completes the turn normally while printing nothing.
Because DeepSeek Harness 0.1.2-rc.1 still writes format 1 and its log still contains assistant/chunk, so the old rendering path finds the text. The commit that changed the contract exists from dsh-v0.1.3-alpha.1 onward, so rolling back restores the old contract without fixing the bridge.
DeepSeek Harness bridges should read assistant/message.message.content and treat it strictly as a settled message body. According to the minimal stable surface in the thread, a channel plugin only needs to subscribe to session.event. Do not parse session logs yourself, and do not depend on the in-process assistant-stream event domain.
DeepSeek Harness empty bodies and unreadable provider/model values are not the same cause. Empty bodies come from a change in the session format projection contract, while unreadable provider/model values belong to config resolution in the composed profile tree. Use dump-config to see whether that section is missing or overridden, and troubleshoot the two separately rather than patching both at once.
Related Terms
- SESSION_FORMAT_VERSION
- SESSION_FORMAT_VERSION is the version number of the DSH session log format. It determines event types and field shapes and is declared centrally in core/session types; the currently published format version is 4.— https://github.com/deepseek-ai/deepseek-harness/discussions/10074
- assistant/message
- Since format v2, assistant/message is the single persistent settlement event for each model attempt. It carries turn, step, the assembled message, the raw stream, and optional usage; the body lives in message.content.— https://github.com/deepseek-ai/deepseek-harness/discussions/10074
- assistant/attempt
- assistant/attempt is an event added in format v2 for failed, retried, cancelled, or stream-error attempts. It carries turn, step, and stream, and adds no message to the model-visible history.— https://github.com/deepseek-ai/deepseek-harness/discussions/10074
- session.event
- session.event is the server-to-client notification in the SDK protocol. Its payload is a sessionId plus one SessionEvent, and it is the minimal stable surface recommended for channel plugins.— https://github.com/deepseek-ai/deepseek-harness/discussions/10074