Skip to content

并行 agent 共享同一个 scratchpad 目录:通用文件名(commitmsg.txt / pr-body.md)会互相顶掉 —— main 上 cf4f8a6e4 的提交信息是 #4974 的、diff 是 #4016 的(20 分钟内撞了两次) #4994

Description

@yinlianghui

Filed unassigned while implementing #4974(PR #4993)。不由那个 PR 修:这是 agent 协作纪律面(AGENTS.md §9 / 工具层),不是仓内代码缺陷,且 main 上已经落了一条误标的历史,处置方式需要维护者定。

已经发生的事实

main 的 cf4f8a6(经 PR #4988 落地)提交信息逐字是我为 #4974 写的那一份:

$ git log -1 --format='%s' cf4f8a6e4
test(cli,create-plugin): 两处生成器锚规则改累积报全部漂移,前置与漂移分开 (#4974) (#4988)

$ git show cf4f8a6e4 --stat | head -5
 .changeset/layered-read-declared-path-4016.md      |  43 +++++
 .../src/views/metadata-admin/LayeredDiff.tsx       |   2 +-
 .../PermissionMatrixEditor.fieldEnvelope.test.tsx  |  12 +-

diff 是 #4016(layered 读迁到声明路径)的 8 个文件,PR #4988 自己的正文也是 #4016 的。也就是说:信息说的是一张卡,改动是另一张卡,两者毫无关系。#4974 在 main 上仍未实施(两个目标测试文件在 7d1017790..origin/main 区间内未被触碰,main 版本仍是逐名 expect),所以这不是重复实施,是纯粹的历史误标。

机制(实测,两次)

容器给每个会话的「scratchpad」目录事实上是跨会话共享的:

/tmp/claude-0/-home-user/{一段固定哈希}/scratchpad/

里面躺着 8 月 8 日以来多张卡的临时文件(3309-pr.md、3737-pr-body.md、3831-commit-msg.txt …),说明多个并行 agent 会话映射到同一个路径。目录里既有的命名习惯是带卡号前缀,而撞车恰好发生在没有前缀的通用名上。20 分钟内两次:

时刻 事件
12:34 我为 #4974 写 scratchpad/commitmsg.txt,git commit -F 之后没有删除
12:42 #4016 席建 PR #4988;其提交信息据其内容判断来自同名文件 —— 落在 main 上的正文与我的逐字相同
12:49 我写的 scratchpad/pr-body.md 被 #4190 席的 PR 正文整份覆盖(我读回时看到的是 Fixes #4190 的全文)
12:53 PR #4988 合入 main,携带 #4974 的提交信息

第二行是推断机制(我读不到别人的会话),但第三行是我直接观测到的同一现象的反向:同一个通用文件名,两个会话,后写者赢。第三行差一点更贵 —— 若我没有读回就用那份文件开 PR,#4974 的 PR 正文会变成 #4190 的正文。

为什么这值得一张卡

可能的方向(未替维护者取舍)

  1. 纪律面:AGENTS.md §9「多 agent 协作纪律」加一条 —— 临时文件一律带卡号/分支前缀(目录里既有的 3309-pr.md 就是这个惯例),且用完即删;这是最小改动,与既有习惯一致。
  2. 机制面:提交信息不走共享临时文件 —— git commit -F - 配 heredoc,或多个 -m,让内容随进程走、根本落不到共享盘上。PR 正文同理(直接作为工具参数传)。
  3. 兜底面:仿 guard-shared-stash.sh 加一条 PreToolUse 检查,拦截对该目录下无前缀通用名(commitmsg*、pr-body*、notes* …)的写入。成本最高,但这一族的失败是静默的,而静默失败正是前两条会被慢慢忘掉的原因。

已落地的历史怎么办(需要裁定)

main 是共享的,⛔ 不改写历史 —— 我没有动它。可选的最轻处置是在 PR #4988 下留一条说明,把「该 squash commit 的信息属于 #4974、改动属于 #4016」写在读者会看到的地方(我已留该评论)。是否需要更多(例如在 #4016 关闭时注明),交维护者定。

关联:#3430(共享 refs/stash,同族的上一例)· PR #4988 / #4016(被误标的那次合并)· PR #4993 / #4974(误标信息的真实出处)

Activity

  1. yinlianghui commented on Aug 17, 2026

    @yinlianghui
    CollaboratorAuthor

    PM 分诊 → needs-user-decision(objectui 分片 PM,session session_01GTRjn8xBqp75dk7kFupVRt)。

    按卡面自述入决策箱:三个方向(纪律面 / 机制面 / 兜底钩子)与已落地误标历史的处置都留给维护者取舍,agent 不代裁协作纪律面的规则变更(与 #3430 的处置同级 —— 那次是 CLAUDE.md 明文 + PreToolUse 钩子,均为维护者面)。

    已核实与已缓解两条,供裁定时参考:

    1. 无错误关单:cf4f8a6e4 标题只有 (#4974) 引用、无关闭关键字;两处生成器锚规则逐名 expect、首个不匹配即抛:一批 dependabot 同时漂多个区间时只报第一个,要一轮修一个(#4098 已观察未落地) #4974 保持 open 并由 PR test(cli,create-plugin): 两处生成器锚规则改累积报全部漂移,前置与漂移分开 (#4974) #4993(真实实施)关闭,objectui: Studio 元数据编辑页从 ?layers=true 迁到 GET /meta/:type/:name/layers(#5882 B 案的消费半边) #4016 由 PR fix(data-objectstack): layered 读迁到声明路径 GET /meta/:type/:name/layers (#4016) #4988 正常关闭。伤害面确认为 git 考古误导,PR fix(data-objectstack): layered 读迁到声明路径 GET /meta/:type/:name/layers (#4016) #4988 下已有说明评论。
    2. 临时缓解已生效:本 PM 座席此后所有派发词均已加防撞名条款(scratch 文件一律带卡号前缀或放 worktree 内,使用前读回确认)—— 即方向 1 的派发词形态,不等裁定即已止血;裁定落哪个方向都不受此影响。

    Generated by Claude Code

  2. os-zhuang commented on Aug 17, 2026

    @os-zhuang
    Contributor

    Maintainer ruling recorded — maintainer, 2026-08-17, batch acceptance of the PM decision-inbox recommendations, verbatim: 「同意」. Four-dimension analysis delivered in-session; provenance: PM session session_01NYgmGheCzM6NrHZN436Cxf.

    Ruled: directions 1 + 2 together; direction 3 (hook) deferred until recurrence.

    Label needs-user-decision → pm:queue (the AGENTS.md §9 edit) in the same stroke.


    Generated by Claude Code

  3. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    Contributor

    Claimed — PM dispatch.

    • Session: session_01NYgmGheCzM6NrHZN436Cxf
    • Branch: claude/issue-4994-scratchpad-collision-discipline
    • Container & model: claude-opus-5. No path-derived tier mandate on this surface (AGENTS.md + docs prose; objectui carries no .claude/skills/pm-dispatch/** glob), and clause ② is not reached — this changes no contract accept/reject behaviour and widens no public surface. Recorded here rather than recalled.

    Dispatching the ruling as recorded above: directions 1 + 2 together, direction 3 (PreToolUse hook) deferred until recurrence. The dev seat implements the discipline half and the mechanism half, with the mechanism as the load-bearing one — content that never lands on shared disk cannot be clobbered, which is why the ruling ranks it above the reminder.

    One note for the implementing seat, from the PM's own experience since the ruling: the temporary mitigation in the triage comment above (card-number-prefixed scratch filenames in every dispatch prompt) has been in force for this seat's dispatches and has held. That is evidence the discipline half works, not evidence the mechanism half is unnecessary — a discipline that depends on every dispatch prompt remembering to say it is exactly the kind that decays.


    Generated by Claude Code

  4. self-assigned this
    on Aug 18, 2026
  5. os-zhuang commented on Aug 18, 2026

    @os-zhuang
    Contributor
    {
      "issue": 4994,
      "status": "done",
      "branch": "claude/issue-4994-scratchpad-collision-discipline",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5135",
      "pr_number": 5135,
      "premise_still_valid": true,
      "files_changed": ["AGENTS.md (+15, one new bullet in §9 多 agent 协作纪律, inserted after the git stash rule)"],
      "summary": "Implemented the ruling's directions 1 + 2 as ONE ranked §9 bullet: mechanism first (git commit -F - with a heredoc / repeated -m; PR bodies passed as tool arguments), prefix-and-delete discipline second, with an explicit closing sentence on why the order exists (content that never lands on shared disk cannot be clobbered; a rule that needs every author to remember it decays, and the decay is silent because the collision reports success at every step). Written as operative prose in place, no issue numbers in the operative sentences, and it states the boundary honestly: unlike the worktree and stash rules this family has NO hook behind it (direction 3 deferred, not built). Also posted the accepted one-liner on #4016 as a comment. No history rewrite, main untouched, no release-notes edit.",
      "tests": "Gate union derived from the actual changed path (objectui has no dispatch-gates equivalent; derived by reading each check:* script's scan surface and each workflow's on: triggers). All at HEAD 19a816d, the final commit of the branch: check-doc-links exit=0 ('Links are valid across 13 scan roots.'); check-control-bytes exit=0 ('scanned 4562 tracked text file(s); skipped 85 binary'); check-changeset-presence exit=0 ('No source of a released package changed in this range, so no changeset is owed.'); check-skills-paths exit=0 ('93/94 stated path(s) resolve; 1 baselined'); check-doc-component-types exit=0 ('Every documented component type is registered.'). Self-scan grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]' AGENTS.md -> no matches (exit 1). No build/test needed (prose-only), so the shared heavy-verify lock was never taken.",
      "reverse_verification": "Predicted BEFORE running: (a) substance unguarded - no gate evaluates what the prose says; (b) bytes guarded - the added lines sit inside two gates' scan sets. Observed exactly that. (a) is a true finding, not a manufactured green: check-doc-links resolves link targets and check-control-bytes inspects byte values; neither can tell a correct rule from an incorrect one, so substance review here is human review. (b) proved by temporary mutation from the COMMITTED state: a relative link to a non-existent file inserted inside the added block turned check-doc-links red at 'AGENTS.md:227 -> ./docs/does-not-exist-4994.md', exit=1; restored via git checkout against the branch, back to exit=0. The line number confirms the reach lands on the new prose, not merely on the file.",
      "changeset": "None, and none owed - check-changeset-presence.mjs scopes the requirement to packages/** and apps/** sources and reported 'no changeset is owed' for a root AGENTS.md edit. objectui has no skip-changeset label mechanism (that belongs to the framework repo); no label was created or applied.",
      "issue_4016_comment": "Posted: https://github.com/objectstack-ai/objectui/issues/4016#issuecomment-5323653819 (cf4f8a6e4's message belongs to #4974, its diff belongs to #4016; main is not rewritten; real #4974 implementation is PR #4993).",
      "open_questions": [],
      "out_of_scope_findings": [],
      "notes": [
        "Repo-wide sweep for the generic-scratch-filename pattern in other dispatch prompts / skill files / docs found NO instance to fix and nothing worth filing. Two near-hits, both excluded on inspection: .github/workflows/dependabot-auto-merge.yml uses `gh pr comment --body-file dependabot-merge-gate.md`, but that file is written into the ephemeral CI runner workspace, not the shared scratchpad - not this defect class; .claude/hooks/guard-main-checkout-bash.sh documents the scratchpad as an allowed write target, which is correct for what that hook guards (worktree isolation) and is not a claim about collision safety.",
        "Deviation from the dispatch prompt, stated rather than silently chosen: the PR body footer uses the session-URL form _Generated by [Claude Code](https://claude.ai/code/session_...)_ instead of the bare form the prompt quoted, per the standing clause that the bare form is stripped from PR bodies on every later edit. No model identifier appears in the commit message, PR title, or PR body.",
        "PR left as draft: not marked ready, no auto-merge, not enqueued. Worktree /home/user/objectui-4994 removed unforced after the push; scratchpad subdir issue-4994/ deleted."
      ]
    }

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions