Skip to content

bug: DSH recall uses the retired "plugin" source kind — every turn fails on DeepSeek Harness 0.1.7 #174

Closed

Description

@MaxwellXiao

Pre-submission checklist | 提交前检查

  • I have searched existing issues and this hasn't been mentioned before | 我已搜索现有问题,确认此问题尚未被提及
  • I have read the project documentation and confirmed this issue doesn't already exist | 我已阅读项目文档并确认此问题尚未存在
  • This issue is specific to MemOS and not a general software issue | 该问题是针对 MemOS 的,而不是一般软件问题

Environment | 环境

Item Value
Plugin @memtensor/memos-cloud-dsh-plugin@0.1.1 (npm latest as of 2026-09-25)
Host DeepSeek Harness 0.1.7-rc.2, profile desktop
Host protocol session format v4 (hostProtocolVersion: 4)
OS / runtime Windows 11 x64, Node 24.21.0

Bug Description | 问题描述

With the DSH plugin enabled, every turn that triggers recall fails. The run aborts with:

SessionFormatError: format v4 message requires a producer-owned source kind

DSH surfaces this to the user only as a failed turn ("本轮运行失败"); no assistant reply is produced.

Note this is not intermittent: MemOS' search/memory treats conversation_id as a relevance signal rather than a hard filter, so once the user_id namespace contains anything, recall returns hits for essentially any query and the plugin becomes unusable while active.

Steps to reproduce | 复现步骤

  1. Install and enable the plugin as a DSH profile bundle, e.g.
    dsh plugin --profile desktop add @memtensor/memos-cloud-dsh-plugin

  2. Set MEMOS_API_KEY (and optionally MEMOS_USER_ID) in ~/.dsh/.env, and add the profile patch row:

    - id: memos-cloud
      name: "@memtensor/memos-cloud-dsh-plugin"
      config:
        apiKeyEnv: MEMOS_API_KEY
  3. Ensure the namespace holds at least one memory. Easiest: complete one turn with recall disabled, so the write-back path populates it — or use a namespace that another MemOS client already filled.

  4. Restart DSH and send any message.

Expected: recalled memories are injected as a <memos-recall> block and the turn completes.
Actual: the turn aborts with the error above.

The session log ends immediately after inbox consumption — no step/start follows:

{"type":"turn/start","seq":564,"data":{"turn":7}}
{"type":"agent/inbox/spliced","seq":565,"data":{"target":"next-turn","start":0,"removedCount":1,"inserted":[]}}

Root cause | 根因

createRecallMessage() builds the injected recall message with the retired shared plugin source kind (published lib/index.js:897):

return createUserMessage({
  source: {
    kind: "plugin",          // ← retired in the v4 session format
    plugin: "memos-cloud",
    form: "recall"
  },
  content: [{ type: "text", text }]
});

That message is injected by installMemosLifecycle's agent/pre-step handler (step 1) via insertRecallBeforeDirectUser, so it enters the session as agent/inbox/spliced.inserted.

DSH's format-v4 admission then refuses the row:

// @deepseek-ai/dsh-session → lib/types/message-sources.js
function source(message) {
  const value = message["source"];
  if (!isSessionFormatJsonObject(value) || typeof value["kind"] !== "string"
      || value["kind"].length === 0 || value["kind"] === "plugin")
    throw new SessionFormatError("format v4 message requires a producer-owned source kind");
}

It is called by assertV4MessageSources for every declared durable message slot, and by assertV4SourceRowAdmission specifically for agent/inbox/spliced.inserted.

In v4 a plugin must attribute its own durable messages with a producer-owned kind. DSH's own v3→v4 converter encodes exactly that rule — unknown third-party plugin names become plugin:<name>, first-party ones keep their name:

function producerKind(plugin, role) {
  if (plugin === "@deepseek-ai/dsh-system-prompt" && role === "system") return "system-prompt";
  const renamed = Object.hasOwn(RENAMED_PRODUCERS, plugin) ? RENAMED_PRODUCERS[plugin] : void 0;
  if (renamed !== void 0) return renamed;
  if (RELEASED_SAME_NAME_PRODUCERS.has(plugin)) return plugin;
  return `plugin:${plugin}`;
}

Other third-party DSH plugins already follow this convention (e.g. dsh-session-sync writes with its own kind).

Because the row is rejected before it can be persisted, the error leaves no trace in the session log — only the failed turn is visible, which makes this hard to diagnose from the user side. A diagnostic hint on stderr would help.

Suggested fix | 建议修复

One line:

   return createUserMessage({
     source: {
-      kind: "plugin",
+      kind: "memos-cloud",
       plugin: "memos-cloud",
       form: "recall"
     },

"plugin:memos-cloud" also passes admission, but a plugin's own producer kind is the convention.

Verified locally: with only that change, the recall message is admitted, persisted, and the turn completes. The injected row then reads:

{"type":"user/message","seq":433,"data":{"source":{"kind":"memos-cloud","plugin":"memos-cloud","form":"recall"}}}

Workaround for users on DSH ≥ 0.1.7 | 用户侧临时方案

  • Set recallEnabled: false in the memos-cloud config. The write-back path (/add/message) creates no session messages and keeps working, so memories still accumulate — only recall is lost.
  • Or apply the one-line patch above locally; note that reinstalling or upgrading the plugin overwrites it.

Additional notes | 补充

  • peerDependencies currently pin @deepseek-ai/dsh-session: ^0.1.0-rc.6 || ^0.1.2-rc.1. pnpm reports Issues with peer dependencies found at install time. DSH's own compatibility pre-check still lets the bundle load (it evaluates prerelease versions with includePrerelease, and 0.1.7-rc.2 falls inside ^0.1.2-rc.1), so the skew is silent until the runtime failure. Widening the range will not fix this bug by itself, but it would make the mismatch visible.
  • The write-back (captureTurn / /add/message) path is unaffected and does not need changes.

中文摘要

在 DeepSeek Harness 0.1.7-rc.2(会话格式 v4)上启用本插件后,每一轮都会失败,报 format v4 message requires a producer-owned source kind,用户侧只看到"本轮运行失败"。

根因:createRecallMessage() 注入召回消息时用的是已废弃的共享 source.kind = "plugin"(lib/index.js:897)。该消息经 agent/pre-step 钩子以 agent/inbox/spliced.inserted 进入会话,被 DSH v4 的持久消息准入直接拒绝。v4 要求插件用自己的 producer kind,DSH 自身的 v3→v4 迁移逻辑对第三方插件生成的就是 plugin:<name>,社区插件也都用自己的名字。

修复只需一行:kind: "plugin" → kind: "memos-cloud"。已验证改完后同一条召回消息可正常写入日志、回合正常完成。

临时方案:把配置里的 recallEnabled 设为 false,可保留记忆回写、仅失去召回。另外 peerDependencies 落后于当前 DSH 版本线,安装时 pnpm 已有 peer 警告。

Activity

  1. Adamaik commented on Sep 30, 2026

    @Adamaik
    Contributor

    Confirming the source-kind diagnosis — and adding two more v4 mismatches, one of which contradicts the note above that the write-back path is unaffected.

    The write-back path does need changes. captureTurn reads the tool call identity from the released tool-result content wrapper:

    const block = event.data.message.content.find((value) => value.type === 'tool-result')
    if (block !== undefined) resultCallIds.add(block.toolCallId)

    v4 no longer emits that wrapper: the id rides the message itself (message.toolCallId, mirrored on message.source.callId) and content holds plain blocks. So resultCallIds stays empty — and because toolCalls() emits a call only once its result has been correlated, tool-call-only assistant messages are dropped as well.

    Measured against a real DSH 0.2.0-rc.2 session, ten completed turns with includeToolMemory: true:

    tool messages captured
    0.1.1 as published 0 — each turn reduced to one user and one assistant message
    with the reader fixed 170

    So under the suggested recallEnabled: false workaround memories do keep accumulating, but only bare user/assistant text: no tool calls and no tool results reach MemOS.

    Agent preset. lifecycle.ts tags writes and recall filters with session.header.agentPreset, which holds only the initial value; current DSH keeps the live preset in the agentPreset session projection, and Session exposes no projection accessor. A session that switches preset mid-flight therefore tags every later memory with a stale preset.

    Fix: #176 addresses all three — the source kind, the tool-result shape, and the preset — while keeping the released shapes working, so a single build serves both runtime lines. It uses plugin:memos-cloud rather than a bare kind only because that is exactly what DSH's own v3→v4 migration assigns this plugin id; happy to switch to the bare form if you prefer.

    pnpm test 168 passed / 4 skipped, tsc --noEmit and pnpm build clean. The pinned @deepseek-ai/dsh-*@0.1.2-rc.1 devDependencies are the last line where both retired shapes were still valid, which is why the suite stayed green; the new tests spell out the current shapes directly instead of bumping them.


    补充确认了本 issue 的根因,另外还有两处 v4 不兼容(其中一处与上面"回写路径不受影响"的结论相反):工具的 call id 在 v4 里挂在消息本身(message.toolCallId),不再包在 tool-result 内容块里,导致工具记忆实际抓到 0 条(真实会话十个回合实测,修复后 170 条),并且"只有工具调用没有文本"的 assistant 消息会被整条丢弃;另外 agent preset 应从 agentPreset session projection 读取,而不是 session header(header 只是初始值)。PR #176 一并修复这三处,并兼容旧格式。

  2. EF1874 commented on Oct 30, 2026

    @EF1874
    Collaborator

    Fixed on main by #176 and #180, which reached main in #185.

    #176 changes the recall source to plugin:memos-cloud and reads both the old and current tool-result shapes. #180 widens the DSH peer range through 0.2.x. #176 used Refs #174, so GitHub did not close this automatically.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions