Skip to content

[metadata-protocol] SysMetadataRepository.publishDraft() 把 draft 清理的全部失败都当「并发发布者已抽走」,静默留下一条永远 pending 的 draft 行 #4981

Description

@xuyushun441-sys

发现于 #4867(修同文件两个计数器的 catch { return 1 })。仅记录,未在该 PR 中修改 —— 不属于同一个编号家族,按 Prime Directive #10 单开,未指派。

现象

packages/metadata-protocol/src/sys-metadata-repository.ts,publishDraft() 里(origin/main 约 665–676 行,#4867 的 PR #4980 之后行号会下移):

    // Drop the draft row — it has been promoted. Tolerate races where
    // a second publisher already drained it.
    try {
      await this.delete(ref, {
        parentVersion: draft.hash,
        actor: opts.actor,
        source: opts.source ?? 'sys-metadata-repo.publish',
        intent: opts.intent ?? 'override-artifact',
        state: 'draft',
      });
    } catch {
      // best-effort: a concurrent publisher may have already drained
      // the draft; the active row's authoritative content is intact.
    }

注释只点名了一个原因(并发发布者已经抽走了 draft —— 这时 delete 抛 ConflictError,确实良性),catch 却吞掉全部失败:连接抖动、超时、权限不足、驱动错误,以及 draft 在此期间被改写导致的 parentVersion 不匹配。这与 #4728 / #4825 / #4867 是同一族形状 —— 一个良性原因赦免了所有原因,只是这里的数字换成了一行残留数据。

后果

publishDraft() 返回成功,active 行也确实是对的(这一点注释说得没错),但 sys_metadata 里那条 state='draft' 的行还在:

  • Studio / Setup 会继续把这个 artifact 显示为「有未发布的改动」,而它其实已经发布了 —— 用户看到一个点了没用的按钮;
  • 下一次 publishDraft() 会拿这条陈旧 draft 当新内容再发布一次,写出一条内容与 active 完全相同的历史事件(甚至可能把已经被覆盖的旧 body 重新推成 active);
  • 没有任何日志,重试不修、重启也不修。

按 AGENTS.md「Degradation log levels」的那一问:降级之后系统对外看起来完全正常,而它声称已经清理的东西并没有清理 —— 属于 error 那一类,不是 warn。

建议(与 #4728/#4825/#4867 一致的形状)

按错误类型判别,只赦免真正良性的那一个:

参考

Activity

  1. claude commented on Aug 3, 2026

    @claude
    Contributor

    🔒 认领:PM 循环第 5 轮
    会话:session_01NrmBxj8rK2uGCnh9aipjwX(账号 xuyushun441-sys)
    分支:claude/issue-4981-publishdraft-drain-discriminate
    Worktree:objectstack-issue-4981
    域:domain:engine
    文件面:packages/metadata-protocol/src/sys-metadata-repository.ts(publishDraft)及同目录测试

    throw-vs-report 裁定(issue 单开的那个问题):只报不抛。理由沿两轴:(1) delete 在 put 提交之后,抛出会把一次已持久化成功的发布谎报成失败,且诱发调用方重试 —— 重试恰恰触发「陈旧 draft 再发布」这条真正的数据伤害路径;(2) 防 AI 错轴:响亮 + 机器可读的部分成功信号让调用方无法把缺口读成良性,#4969(今日先例,维护者已过目)同形 ——「决策成立,但绝不静默」。判别按 #4728/#4825/#4867 族:仅 ConflictError / 行已不存在静默;其余 error 级上报后果与修复动作。若要加结构化字段而它构成 spec 契约变更 —— 本轮禁区,退回 log-only 并报告。
    ⛔ spec/** / protocol.ts 零改动;releases/ 零触碰。


    Generated by Claude Code

  2. claude commented on Aug 3, 2026

    @claude
    Contributor

    🔁 接手记录:原 dev agent 因会话额度耗尽被杀(不可恢复),worktree objectstack-issue-4981 与未提交改动完好。按 worktree 接手协议派新 agent 继续 —— 同一认领、同一分支,本条为交接记录,原认领评论继续有效。会话:session_01NrmBxj8rK2uGCnh9aipjwX。


    Generated by Claude Code

  3. claude commented on Aug 4, 2026

    @claude
    Contributor

    ✅ 验收通过 —— PR #5025(draft,CI 跑完入队);接手协议全程执行

    对照实际 diff 复核(5 文件,+650/-14;⛔ spec / protocol.ts / releases 零触碰,逐一核实):

    1. 接手甄别做实了:原 agent 死于变异验证的「Restoring」瞬间,接手者逐 hunk 对照确认工作树里已无故意改坏的代码,而非盲信遗留状态。
    2. 判别按裁定落地且比裁定更细:ConflictError 的两个分支都论证了良性(actualHead===null = 并发发布者已抽走;head 不同 = 期间存了更新的 draft,更不能删);其余失败 error 级点名孤儿 artifact + 后果 + 处方,原因附队。
    3. 只报不抛 + 机器可读信号:draftDrainFailed 可选字段 —— 且 dev 核实了裁定第 2 条的前提:promoteDraft 是 class-only 方法,不在 MetadataRepository 接口、不在 protocol.ts、不在 spec —— 非破坏性附加成立,不是宣称。
    4. 裁定第 3 条的便宜半边白拿:put() 的同 hash 短路已天然自愈「active 未变时的下一次 publish」,补 pin 测试;贵的半边(内容上与真实待发布不可分)明确不猜,写进 PR 正文作跟进项 —— 这个不扩权的判断正确。
    5. 变异验证从零重做、双向:盖毯 → durability 门禁 + 5 测试红;删豁免 → 恰好 2 条良性用例 + 判别器测试红(静默半边同样被钉);恢复后 byte-identical。dropPromotedDraftRow 具名并入门禁台账(11 seams 全响)。

    CI 绿后转 ready 入合并队列。


    Generated by Claude Code

  4. added a commit that references this issue on Aug 4, 2026
  5. claude commented on Aug 4, 2026

    @claude
    Contributor

    ✅ 返工验收通过(HEAD 92e0623)—— CI 绿即入队


    Generated by Claude Code

  6. added a commit that references this issue on Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions