Skip to content

[Bug] scope.js 裸读 ctx.workspaceRegistry 触发 "cannot get property without inject":每次写入刷告警且 workspace_scope 退化为原始 cwd #487

Closed

Description

@user070212

现象

每次写入记忆都会打一条 warn,且记忆的 workspace_scope 永远退化成会话原始 cwd:

[dsh-mneme] workspaceRegistry lookup failed, falling back to session header cwd:
Error: cannot get property "workspaceRegistry" without inject

环境

  • @modusensus/dsh-mneme 0.9.2
  • DSH host 0.2.0-rc.2(dsh --version 输出)
  • Windows 11 / Node 24.17
  • profile 里 autoInject、autoDream 等均为默认配置,未做特殊改动

复现

  1. 在任意 DSH profile 里启用 mneme(0.9.2),保持默认配置;
  2. 调用一次 memory_save(或任何会落库的写入路径);
  3. 观察 host 日志 → 每次写入都出现上面那条 warn。

定位

lib/scope.js:67 直接读 ctx 属性:

const list = ctx?.workspaceRegistry?.list?.();

而 lib/index.js:45 的 inject 声明里没有 workspaceRegistry:

export const inject = ["tools", "systemPrompt", "llm", "agentDefaultModel", "commands", "webServer"];

cordis 4 的 ctx 是 Proxy,裸读未 inject 的属性会抛错(而不是返回 undefined),所以这里必然走到 lib/scope.js:77 的 warnOnce 分支。

值得注意:lib/index.js:577-579 的注释已经写明这种情况的推荐写法(webServer 这个可选依赖就是用 ctx.reflect.get 处理的):

webServer 可选依赖:cordis 4 的 ctx 是 Proxy,直接访问未在 inject 声明的属性会抛 "cannot get property without inject"(不会返回 undefined),所以不能用 if (ctx.webServer) 守卫。ctx.reflect.get 是 cordis 提供的免 inject 读取方式。

影响

  1. 日志噪声:每次写入刷一条 warn,正常运行时无法从日志里看出真正的问题;
  2. 作用域精度下降:workspaceRegistry 反查本来是"成员资格 = id 命中 + header canonical cwd 校验过,path 已 realpath 归一"(scope.js 注释原文),退化成 header.cwd 后,存进记忆的 workspace_scope 是原始 cwd 字符串而非规范路径,成员资格校验被跳过。单工作区场景影响小,多工作区/多写法路径时可能对不上。

建议修法

两个方向,取决于 workspaceRegistry 在这个宿主里是硬依赖还是可选依赖:

A. 若为可选依赖(推测如此,因为缺失时已有降级路径)——与 webServer 保持一致,改用免 inject 读取:

let list;
try {
  list = ctx?.reflect?.get?.("workspaceRegistry")?.list?.();
} catch { /* 未注册或缺 inject,走 cwd 回退 */ }

B. 若为硬依赖——加进 inject:

export const inject = ["tools", "systemPrompt", "llm", "agentDefaultModel", "commands", "webServer", "workspaceRegistry"];

我倾向前者:现有代码已经选择了"失败回退 cwd"的软依赖语义,用 ctx.reflect.get 能把那条 warn 一起消掉;如果只是加进 inject,那么在没有该服务的宿主(如 headless / daemon 模式)上会让整个插件起不来——index.js:39-42 的注释提到 v0.7.23 就踩过"移出 inject 导致崩溃"的坑,同一个取舍。

补充

不是安全/数据损坏问题——写入不会失败,记忆也不会丢,只是归属字段不精确 + 日志噪声。谢谢!

Activity

  1. added
    bug功能异常、崩溃、数据错误(agent 诊断 → 人工确认)
    on Oct 19, 2026
  2. modusensus commented on Oct 19, 2026

    @modusensus
    Member

    谢谢报告,定位是准的。我们独立核实后确认这确实是个 bug,并且已经开工修。下面三点补充,其中一点和报告里的描述不一致。

    一、确认成立

    src/scope.js:67 直接读属性:

    const list = ctx?.workspaceRegistry?.list?.();

    src/index.js:45 的 inject 声明里确实没有 workspaceRegistry:

    export const inject = ["tools", "systemPrompt", "llm", "agentDefaultModel", "commands", "webServer"];

    cordis 4 的 Proxy 在读取未注入属性时会抛错而不是返回 undefined,抛错点在 @deepseek-ai/cordis 的 lib/index.js:676:

    const error = new Error(`cannot get property "${prop}" without inject`);

    所以这里必然进入 src/scope.js:75 的降级分支。这段代码自 v0.8.0 引入(同 src/scope.js 的 A1 提交)起一直如此。

    二、但 warn 不是「每次写入一条」

    src/scope.js:25 的 warnOnce 是按解析器实例去重的,而解析器在三个工厂里各建一次(createTools、createInjector、createSummarizer)。所以全程最多 3 条 warn,与写入次数无关。

    用一个复刻 cordis Proxy 形状的 ctx(读未声明属性即 throw)连打 20 次写入,实测:

    warn 条数: 1
    warn 原文: [dsh-mneme] workspaceRegistry lookup failed, falling back to session header cwd: Error: cannot get property "workspaceRegistry" without inject
    20 次结果是否全同: true
    

    如果你那边看到的是每次写入都刷,那可能是宿主在每次写入时重建了插件实例、或者 logger 被换过——如果是这样,麻烦贴一下相邻几条 warn 的时间戳,这本身是另一个值得查的问题。但按当前代码,正常进程内是只打一次。

    三、真实损失比报告里写的更重

    报告把它归为「日志噪声 + 字段不精确」,但宿主其实是提供了这个服务的——宿主文档(cordis 插件开发指南的 workspace 包表)里写着 @deepseek-ai/dsh-workspace 提供 ctx.workspaceRegistry。也就是说服务存在、只是没在 inject 里,用免 inject 读取能真的把它读回来。

    实测同一形状下两种写法:

    现状(裸读):        {"agent_scope":"coder","workspace_scope":"D:\\raw"}
    reflect.get 取到:    [{"path":"C:\\canon","sessionIds":["s1"]}]
    

    所以修好之后不只是消 warning,而是把 registry 反查那两层能力一起恢复了:realpath 归一(workspace_scope 存的是规范路径而不是原始 cwd 字符串)和成员资格校验(id 命中 + canonical cwd 校验)。这两项从 v0.8.0 起一直没生效,单工作区感觉不到,多工作区或路径写法不一致时就会对不上。

    四、修法按 A,但写法比建议里的更严一点

    同意选 A(保留软依赖语义,不加注入)。有两点要偏离报告给的示例代码:

    1. 不要只包一层 try { ctx.reflect.get(...) }。cordis 的 reflect.get(name, strict = true) 在服务未提供时返回 undefined,并不抛错(lib/index.js:763-772)。真正要防的是旧宿主里 ctx 是普通对象、根本没有 reflect。仓库里已有两处同一先例(src/api.js:205、src/index.js:583):

      const webServer = typeof ctx.reflect?.get === "function" ? ctx.reflect.get("webServer") : ctx.webServer;

      这里照抄同一形状最稳。

    2. 不要加进 inject。理由和报告一致,而且本仓有过实战教训:src/index.js:39-44 的注释记着,v0.7.23 把 webServer 移出 inject 想支持 headless,结果 cordis 不再等待服务就绪,桌面端直接崩在 apply 阶段。加 workspaceRegistry 会走同一条死路。

    五、测试为什么没拦住

    现有 test/scope-storage.test.js 里三处 registry 测试都用普通对象伪造 ctx(ctx: { workspaceRegistry: { list: ... } })——普通对象裸读不抛错,返回正常数组,所以这条路径在测试里看着是通的。

    修复会带一个「读未声明属性即抛错」的 Proxy 形状的 ctx 回归锁,两个方向都钉住:

    • 服务存在时必须取回 canonical path,不能降级到 header cwd;
    • 服务缺失时必须静默降级到 header cwd,且不刷 warn、不阻塞写入。

    修好后会在这个 issue 里回一条,带上合并的 PR 号。

  3. moved this from 📋 Backlog to 🔨 In Progress in dsh-mneme 任务看板on Oct 19, 2026
  4. modusensus commented on Oct 19, 2026

    @modusensus
    Member

    修复已提交,PR #488。等你复核。

  5. added a commit that references this issue on Oct 19, 2026
  6. moved this from 🔨 In Progress to ✅ Done in dsh-mneme 任务看板on Oct 19, 2026
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

    bug功能异常、崩溃、数据错误(agent 诊断 → 人工确认)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions