Skip to content

0.10.0:MemoryDrawer 未声明 provenance 状态,打开记忆详情必崩(ReferenceError: provenance is not defined) #553

Closed

Description

@strange2000

现象

macOS 15.7.1 + DSH NEXT,@modusensus/dsh-mneme 0.10.0。

点开侧边栏的记忆入口 → 记忆详情抽屉(MemoryDrawer),渲染进程直接崩,DSH 整个重启。库里数据无损,但会话被打断。

DSH 日志(两条紧邻):

[renderer][error] ReferenceError: provenance is not defined
[renderer][error] slot entry crashed in 'sidebar.footer.action': ReferenceError: provenance is not defined

100% 复现,不是偶发。

定位(lib/client.js,0.10.0)

  • :2791 function EntityPanel({ t, focusEntity, onJumpMemory }) 内部 :2795 有
    const [provenance, setProvenance] = useState(null); // null = still loading
  • :6484 function MemoryDrawer({ t, memory, conflict, ... }) 的 useState 区有
    editing / title / content / importance / agentScope / workspaceScope / confirmWiden / confirmDelete / busy / savedTick / saveError / copied / entities / undoPreview / undoBusy / undoError / undoStillDry,
    没有 provenance
  • 但 MemoryDrawer 内部却在用:
    • :6522-6533(#512 的归因 fetch)
      useEffect(() => {
        let cancelled = false;
        setProvenance(null);                        // ← 未定义
        apiFetch("/api/dsh-mneme/memory/provenance?id=" + encodeURIComponent(memory.id))
          .then((res) => (res.ok ? res.json() : null))
          .then((d) => {
            if (cancelled) return;
            if (d && Array.isArray(d.windows) && Array.isArray(d.mints)) setProvenance(d);  // ← 未定义
          })
          .catch(() => { if (!cancelled) setProvenance(null); });   // ← 未定义
        return () => { cancelled = true; };
      }, [memory.id]);
    • :6714 渲染「蒸发归因」区块
      provenance !== null && h(react.Fragment, null, ...)   // ← 未定义
      (provenance.mints / provenance.windows 在 :6716 :6719 :6723 继续被读)

渲染先于 effect,所以最先抛出来的是 provenance is not defined;就算渲染放过,effect 也会在 setProvenance 上炸。

看起来是加「归因」区块时,useState 落在了 EntityPanel,而 fetch effect 和渲染块落在了 MemoryDrawer。

影响

  • 打开记忆详情必崩
  • 崩的是 renderer,会把整个宿主带走 → DSH 重启(后端 pid 换掉、进行中的会话被打断)
  • 库本身不受影响。崩溃当刻的实测:27 张表齐全、header 偏移 18/19 = 02 02(WAL 正常)、-wal 243 KB、integrity_check = ok

同时提一个更广的问题:一个 slot 内的组件抛错能带走整个 renderer,成本有点高。如果有 error boundary 包住 slot entry,这类错误就只坏一块面板。

建议修法

在 MemoryDrawer 的 useState 区补一行即可:

const [provenance, setProvenance] = useState(null); // null = still loading

EntityPanel 里那份如果确实没在用,建议删掉,避免同名状态在另一个组件里重现这个坑。

环境

  • @modusensus/dsh-mneme 0.10.0(registry 2026-11-02T11:22:12Z 发布)
  • macOS 15.7.1,DSH NEXT
  • 记忆库在 exFAT 共享卷上(journal_mode=WAL),非 ?immutable=1 打开

Activity

  1. added 3 commits that reference this issue on Nov 3, 2026
  2. strange2000 commented on Nov 3, 2026

    @strange2000
    Author

    确认修复,谢谢 🙏

    macOS 端已从 0.10.0 升到 0.10.1(npm 2026-11-03T06:51:58Z 那版),核对结果:

    • lib/client.js:6621 的 MemoryDrawer 里已补上 const [provenance, setProvenance] = useState(null); // null = still loading,setProvenance(...) 和 provenance !== null && ... 现在都落在同一作用域
    • 点侧边栏记忆入口 → 记忆详情:正常打开,不再抛 ReferenceError
    • DSH 日志里 provenance is not defined 自升级后 0 次(升级前当天有 2 次,每次都带走整个 renderer)

    顺带确认一句:升级后库本身没有任何变化需求,27 张表齐全、-wal 正常、integrity_check = ok,纯粹是 UI 层的问题。

    另外仍想提一句(不影响本 issue 关闭):slot 内组件抛错能带走整个 renderer,是因为没有 error boundary 兜底。如果后续能在这个层面加一层保护,单个插件的 UI bug 就不会再让整个宿主退出——那个代价比补一行 useState 大得多。

  3. strange2000 commented on Nov 3, 2026

    @strange2000
    Author

    GUI 实测确认:0.10.1 里点开侧边栏记忆入口、打开记忆详情不再崩溃(此前 0.10.0 必崩,日志 2 次 provenance is not defined,升级后 0 次)。先关单,若需要更多现场随时可以重开。

  4. modusensus commented on Nov 4, 2026

    @modusensus
    Member

    谢谢实测确认。修复这件事我这边独立核了一遍,不是只看你的行号:

    • v0.10.1 的 dsh-mneme/lib/client.js 是 9578 行,:6621 就是 const [provenance, setProvenance] = useState(null); // null = still loading,setProvenance 另在 :6644 :6649 :6651;
    • 不止 tag——我把 npm 上那个 dsh-mneme-0.10.1.tgz 拉下来解包比对,包内 lib/client.js 同样 9578 行、行号与内容逐字相同,gitHead 也指向 tag 那个提交,所以「发布的包和仓库对得上」这一条也成立;
    • 反向对照 v0.10.0:setProvenance 在三处被调,而唯一的声明在 :2795(EntityPanel 里那份),MemoryDrawer 一个声明都没有。所以「渲染先于 effect,最先抛 provenance is not defined」的机理成立;
    • 回归锁也进了发布包:test/client.test.js 里有 every client component declares the setters it calls (issue #553)。它不是锁一个名字,而是按组件扫「调了 setXxx( 却没有声明」,因为这次躲过去的是语法检查和原有全部用例——同名 state 落在隔壁组件里,肉眼与静态检查都不容易看出来。

    关于你提的 error boundary,得纠正一处

    你说「一个 slot 内的组件抛错能带走整个 renderer,因为没有 error boundary 兜底」。边界其实存在:宿主里有个 SlotErrorBoundary(react.Component 子类),componentDidCatch 打的正是

    console.error(`slot entry crashed in '${this.props.slotKey}':`, error)
    

    ——也就是你日志里的第二行。换句话说,那一刻边界已经触发并记了日志,却没能挡住 renderer 重启。它还会把崩溃的 entry 撤下(onEntryError → reportEntryError(..., { abdicate: kind !== "chain" })),渲染成一个 data-slot-error 的空锚点。

    所以「再加一层边界」这个修法,对你观察到的现象没有用;要追的是为什么边界拦住了、renderer 还是重启了——可能是崩溃发生在边界之外(比如 overlay 那条 createPortal 到 document.body 的路径),也可能是撤下 entry 后的重渲染又炸了一次。

    这一层我没有证据,不打算顺着因果往下写。我这台机器没有 macOS + 那个宿主版本,复现不了,只能读到宿主打包文件里的边界实现。如果你之后还能碰到,麻烦留一下重启前后的完整 renderer 日志(尤其是有没有第二个不同的 slot entry crashed in 或别的 error 行),那能定分叉点。

    另外 MemoryOverlay 确实是走 portal 挂在 body 上的,不进 slot 树——如果那次崩溃发生在抽屉之外的面板里,边界根本盖不到它。这条只是可能性,不是结论。

    先关单是对的,需要更多现场随时重开。

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