Skip to content

chore(submodule): bump _external/clawdbot fc6a22319 → a67f0df63 (commonly_attach_file + 24 more) - #418

Closed
samxu01 wants to merge 1 commit into
mainfrom
chore/bump-clawdbot-submodule-attach-tools
Closed

chore(submodule): bump _external/clawdbot fc6a22319 → a67f0df63 (commonly_attach_file + 24 more)#418
samxu01 wants to merge 1 commit into
mainfrom
chore/bump-clawdbot-submodule-attach-tools

Conversation

@samxu01

@samxu01 samxu01 commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

The _external/clawdbot submodule pointer is out of date relative to the openclaw fork's rebase-2026.3.29 branch by 25+ commits. Most critically, it's missing the commonly_attach_file extension tool — the kernel-level affordance that the backend's pod-context cue actively tells agents to call.

Root-cause story (smoke 2026-05-20 cycle 6)

Aria reported the error:

Validation failed for tool "commonly_post_message": content: must have required property 'content'

Investigation today: there is no commonly_attach_file tool definition anywhere in our currently-pinned submodule SHA (fc6a22319). The pod-context cue in agentMentionService.formatPodContextFrame tells agents to call commonly_attach_file({ podId, filePath, message }) — which at fc6a22319 has no matching tool. Aria's model fell back to commonly_post_message and stripped message (assuming alias for content), producing the schema-validation error.

CLAUDE.md memory + ADR-013 Phase 0b BOTH document commonly_attach_file as shipped (Nova nailed e2e attach 2026-05-08 deploy d7a68926). The submodule SHA appears to have regressed since.

What this bump brings in (selected; full list in commit message)

  • commonly_attach_file extension tool (8b50281b8) + workspace-resolution fix (18c2967c7)
  • commonly_read_attachment one-shot file read (ae3235c12)
  • commonly_log_cycle ADR-012 Phase 2 append-only cycles writer (a67f0df63)
  • commonly_open_dm — autonomous agent-to-agent DM initiation (11878b43c)
  • commonly_read_my_memory + commonly_save_my_memory — ADR-003 Phase 2b (826d46475)
  • acpx_run LiteLLM routing (6c99dc312)
  • [[reply_to]] strip in commonly_post_message (480298e30)
  • Pod-scoped sessions + websocket + client refactor (5a05c963d)
  • TOOL_ROUTING_HINT, SELF_IDENTITY_NOTE injection, gh CLI, ripgrep (519cdb664)
  • 17 other commits — see git log fc6a22319..a67f0df63 -- extensions/commonly/

Why now

Multiple bugs from yesterday's smoke trace back to these missing tools:

  • Aria's content required error → no commonly_attach_file (this PR)
  • Theo/Nova/Pixel/Ops not consulting commonly_open_dm for cross-runtime collab → no commonly_open_dm (this PR also fixes that — combined with PR feat(agentMentionService): inline cross-runtime consultation cue #416 the consultation cue)
  • Capability-denial patterns → many agents missing the openclaw tool block enhancements

Risk

The submodule range includes 25+ commits. Some may be behavior-changing. CI on this PR should catch obvious regressions. Recommend deploy to dev first via Deploy Dev workflow before any prod-like rollout.

Test plan

  • CI green
  • After deploy: smoke-test that commonly_attach_file({podId, filePath, message}) produces a real File row + [[upload:fileName]] directive in chat
  • Verify openclaw agents can resolve commonly_open_dm mid-turn

🤖 Generated with Claude Code

Brings in 25+ commits on rebase-2026.3.29 including the tools the
backend's pod-context cue + ADR-013 Phase 0b assume are available:

- 8b50281b8 feat(commonly): commonly_attach_file extension tool
- 18c2967c7 fix(commonly): commonly_attach_file workspace resolution (#6)
- ae3235c12 feat(commonly): commonly_read_attachment tool (#5)
- a67f0df63 feat(commonly): commonly_log_cycle — ADR-012 Phase 2 (#7)
- 11878b43c feat(commonly): commonly_open_dm — autonomous a2a DM (#1)
- 826d46475 feat(commonly): commonly_read_my_memory + commonly_save_my_memory
- 6c99dc312 fix(commonly): route acpx_run through LiteLLM via opencode
- 480298e30 fix(commonly): strip [[reply_to]] tags in commonly_post_message
- 518415ab1 fix(commonly): wire replyToId through message pipeline
- 519cdb664 feat(commonly): TOOL_ROUTING_HINT, SELF_IDENTITY_NOTE, gh CLI
- 5a05c963d feat(commonly): pod-scoped sessions, websocket, client refactor
- 16717d8c8 fix(commonly): acpx_run — catch rate-limit in resolved stdout
- (more — see git log fc6a223..a67f0df63 -- extensions/commonly/)

Root-cause smoke 2026-05-20 cycle 6: Aria reported
'Validation failed for tool commonly_post_message: content required'.
That error came from her runtime falling back to commonly_post_message
because commonly_attach_file DID NOT EXIST in the deployed image at
fc6a22319. The pod-context cue in agentMentionService.formatPodContextFrame
tells agents to call commonly_attach_file({ podId, filePath, message }),
which AT fc6a22319 had no matching tool. CLAUDE.md memory and ADR-013
Phase 0b document this tool as shipped (Nova nailed e2e attach
2026-05-08 deploy d7a6892) — the submodule SHA appears to have
regressed since then.

Same submodule bump also unblocks the 5 missing tools cloud-codex / MCP
runtimes already advertise. Should be deployable as a Deploy Dev
workflow run after merge.
@samxu01 samxu01 closed this in 0168f01 May 21, 2026
samxu01 pushed a commit that referenced this pull request Aug 4, 2026
Fourth rewrite of this comment in one afternoon, and the reason is the finding:
the pin is not a stale pointer nobody moved. It has moved 15+ times and has
crossed lineages repeatedly.

  2026-05-09  f4b7a48  a67f0df6  BRANCH   log_cycle ARRIVES
  2026-05-17  b6a811b  fc6a2231  main     LOST      (bump was for react_to_message)
  2026-05-21  0168f01  a67f0df6  BRANCH   RESTORED  (#418, explicitly)
  2026-05-24  d6e63b2  84549161  main     LOST      (bump was for bundled-skills)
  2026-06-26  a3de6d0  00821479  main     current

Three corrections to what this file said an hour ago:

- not "never pinned" -- pinned twice, and it worked twice
- not "nobody looked at the gitlink" -- #418's subject is literally
  `bump _external/clawdbot fc6a22319 -> a67f0df63`. Somebody caught this exact
  regression on 05-21. A bundled-skills bump undid it three days later.
- not "a stale fork to avoid" -- the branch was a deliberate target twice

Nobody was negligent. A submodule bump surfaces the tool it was made for and
says nothing about the five it trades away; the diff is one line of hex.

Cross-validated by @ux-lead against per-agent last-cycles-append timestamps:
writes cluster at 05-09..05-13 and 05-21..05-23, both strictly inside a
branch-pinned window, nothing outside them. Mongo and the submodule log agree
to the day.

Remedy restated: not a bump in either direction, but ending the divergence.
Anything less leaves the next unrelated bump free to swap the set back --
which is what happened twice after #418 had already fixed it.

Gitlink history read here from `git ls-tree` at each commit that touched
_external/clawdbot; windows and cycles correlation from @ux-lead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
samxu01 pushed a commit that referenced this pull request Aug 4, 2026
… paragraph

The comment in presets.ts describing the openclaw lineage skew was rewritten
FOUR times in one afternoon, each version confidently wrong in a different
direction. Not carelessness at any one step -- every version was verified
before it shipped. A claim about another repo's state does not get fixed; it
decays, and prose has no mechanism to notice.

scripts/verify-moltbot-tool-contract.js resolves the pinned submodule tree,
parses the tool DECLARATIONS out of extensions/commonly/src/tools.ts, and
fails when a tool the cycles trailer instructs moltbots to call is not among
them. Required tools are derived from CYCLES_REFLECTION_TRAILER itself rather
than restated, so editing the trailer to name a different tool is covered
without touching the script.

Would have fired on 2026-05-17 and again on 2026-05-24 -- the two unrelated
bumps that swapped the lineage back after #418 had fixed it by hand.

Design notes that are load-bearing:

- Declarations, not mentions. `name: "commonly_x"` counts; the string
  appearing in a description does not. The original defect was a name in
  prose asserting a capability, so a parser that accepts prose reproduces
  that defect inside the guard against it. Mutation-checked: loosening the
  regex reds exactly the prose test.
- Exit 2 for "cannot verify" (submodule absent, or zero declarations parsed
  from a non-empty file), never 0. Four instruments returned clean zeros
  today whose controls also returned zero; an unrun check must not look like
  a passing one.
- Scoped to the trailer, not the agentMentionService cues, because #818 is
  changing those and a guard straddling an open PR is a merge conflict rather
  than a safeguard. Widening point documented in the header.

NOT WIRED TO A WORKFLOW YET, deliberately. Only deploy-dev.yml checks out
submodules, and the check fails there today because the regression is live --
so wiring it now means either a blocked deploy or a non-blocking check, and a
check that cannot fail is exactly the decorative-config defect this whole
investigation is about, one layer up. It belongs in the reconciliation PR,
where it goes green the moment it goes live.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lilyshen0722 added a commit that referenced this pull request Aug 4, 2026
… bump (#831)

* fix(agents): deliver the cycle directive on the default heartbeat path

10 of 27 deployed moltbots had no cycle-reflection directive in their
HEARTBEAT.md, and one (theo) carried a fossil that named
`commonly_write_agent_memory` as the cycles writer — a tool that writes the
whole memory envelope and cannot append to `cycles` at all.

The trailer itself was never wrong. `withCyclesDirective` was applied at
exactly two sites, `provision.ts:294` and `reprovision.ts:137`, both inside
`matchedPreset?.heartbeatTemplate ? {...}`. Preset ids are role names
(`backend-engineer`, `dev-pm`, …) and the fallback match is
`p.id === normalizedInstanceId`, so for any agent installed without an
explicit `presetId` the match failed, `ensureHeartbeatTemplate` fell through
to the raw default, and the delivered file structurally could not carry the
directive. The 17/10 split is exactly presetId-set vs presetId-unset.

Two fixes, because either alone leaves the fleet broken:

1. Apply the trailer to the default branch in `ensureHeartbeatTemplate`, so a
   preset match failure can no longer silence the directive.
2. Count "missing the directive" as stale. Previously the only rewrite
   triggers for a non-forceOverwrite reprovision were two 2026-era marker
   strings, so a pre-trailer HEARTBEAT.md survived every reprovision
   indefinitely — fix 1 would otherwise only help freshly provisioned agents.
   This is staleness, not customization: operator-edited files are already
   short-circuited upstream by `customizations.heartbeat === true`.

The grep marker is exported from the trailer's own module rather than
re-typed at the grep site. A second literal would drift silently, and the
drift would present as "nothing to rewrite" rather than as a failure.

Tests assert on the base64 payload actually written into the gateway pod,
not on the source that composes it — the defect was invisible at every layer
above the delivered file. Both fixes mutation-checked: reverting each one
fails its own test and nothing else.

Not fixed here: `agentProvisionerService.ts` (non-k8s path) has the same
missing import; the scheduler's inline cue is #818's surface.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(agents): record hand-authored HEARTBEAT.md as a customization

The staleness clause in the previous commit claimed operator edits were
"already short-circuited upstream by customizations.heartbeat === true".
That guard was never set by the endpoint that performs the edits.

`routes/registry/files.ts` lets any pod member or creator POST a
HEARTBEAT.md; it writes the file verbatim and persists
`AgentProfile.heartbeatContent`, and never touches `customizations`. The
flag only ever arrived from `installationConfig.customizations` at provision
time, and the frontend only reads it for a badge. So `skipHeartbeat` stayed
false for exactly the files a user had hand-written, and the new
"missing the directive ⇒ stale" clause would have overwritten them.

Fixed at the cause rather than by narrowing the clause: the endpoint that
accepts a user's file now records the file as user-owned. A `reset` clears
the flag, which is the one case where the provisioner should own the file
again.

This also explains the 5 agents that carry the cycle trailer while matching
no preset id (fakesam/liz/tarik/tom, all mtime 2026-05-24; ops 2026-07-29).
They were written through this endpoint, which is why no store records a
presetId for them — it never sets one. Found by @ux-lead running the
control group I had left out of my own query.

Mutation-checked: removing the updateOne fails both new assertions and
leaves the pre-existing write test passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(security): rate-limit the HEARTBEAT.md write endpoint

CodeQL flagged js/missing-rate-limiting (high) at files.ts:211 on this PR
and NOT on main — the previous commit's `AgentInstallation.updateOne` added
a database access to an unlimited route handler, which is what tripped it.
Self-inflicted, so fixed here rather than deferred.

The POST heartbeat-file handler is more expensive than the read surface the
existing inspectorRateLimit guards: it execs into the gateway pod to write
the PVC and writes two Mongo documents. 30/min per user rather than 120.

Declared in this file on purpose — CodeQL's query only recognises the
middleware when it is declared alongside the route registration, per the
note already at the top of the file.

Not widened: the identity-file POST has a similar shape and is not alerted;
files.ts:368 carries a pre-existing alert on main. Both are out of scope for
a fleet-provisioning fix and neither was introduced here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(security): correct the CodeQL same-file claim that propagated a non-fix

The header comment asserted that CodeQL's js/missing-rate-limiting query
"only recognises the middleware on the SAME file as the route registration."
The repo's own evidence contradicts it: alert #1658 has been open against
the a2a-dms route since 2026-05-11, and that route has carried
inspectorRateLimit inline the entire time.

I read the comment as settled, copied the pattern for the heartbeat POST,
and produced alert #1720 instead of clearing anything. The comment is the
surface that generated the error, so it is the thing to fix — a correction
filed only in the PR thread would reach nobody writing the next route.

The limiters stay. 30/min on an endpoint that execs into the gateway pod
and writes two Mongo documents is correct on the merits whether or not a
scanner models it. What changes is the claim about why it is there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(security): run the workspace rate limiters before auth, not after

CodeQL alert #1720 stayed open after `workspaceWriteRateLimit` was applied
inline, and #1658 has been open against the a2a-dms route since 2026-05-11
with `inspectorRateLimit` applied the whole time. A header comment in this
file blamed same-file placement; the previous commit corrected that to
"unproven". The actual mechanism is ordering.

`js/missing-rate-limiting` anchors to the first middleware in the chain, and
`auth` does a Mongo lookup — so a limiter placed after `auth` leaves that
lookup unprotected. Cross-tabbed against main: of ~37 routes with the limiter
before auth, zero are flagged; of the 9 with it after, 6 are flagged,
including both routes here. Three after-auth routes escaped and I did not
chase them.

The scanner was reporting something true, so this is a real fix rather than a
silencing. Reordering costs `req.userId`, which auth had been setting, so the
key generators now hash the Authorization header — the idiom
`routes/messages.ts` already uses for its pre-auth limiters, and the reason
those routes are clean. Per-caller isolation is preserved; unauthenticated
callers still fall back to ipKeyGenerator.

Test asserts the limiter sits at index 0 on both routes. Position, not
presence: "a limiter is somewhere in the chain" stays green through exactly
the regression this exists to catch. Mutation-verified — reverting either
route's order turns it red. Matching on path alone silently resolved to the
GET route, which has no limiter, so it matches on method too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(security): correct the rate-limit comment at the source of the belief

Comment-only; no behavior change in this file.

routes/messages.ts credited its clean CodeQL status to inlining dualAuth in
the same file the limiter is declared in. That is the wrong cause, and the
belief propagated: it was copied into routes/registry/files.ts, where the
limiter WAS same-file and the routes were flagged anyway — #1720, and #1658
for three months.

What keeps these routes clean is order. The limiter precedes the auth
middleware, so the Mongo lookup auth performs is itself covered. Cross-tab
against main: ~37 routes with the limiter before auth, none flagged; 9 with
it after, 6 flagged.

Correcting it here rather than only in files.ts, because this is the copy the
next author reads before writing the next route — fixing the diagnosis where
it was diagnosed leaves the surface that generates it untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(security): raise the workspace write cap to 120, correct the last 3 copies

Two changes, both from @sprint-review's review.

1. workspaceWriteRateLimit 30 -> 120 (and the 429 message, which would
   otherwise have stated the old cap).

   30 was a guess. Measured: the UI's five call sites are all single-agent
   from an open dialog, so no human reaches 30 by clicking. The path that can
   is a scripted fleet-wide heartbeat repair — the operation this endpoint
   exists to make correct — at one POST per agent under one operator token.
   The fleet is 27 agents. 27 of 30 is not headroom; it is a coincidence that
   expires when the fleet passes 30, and it fails by killing a repair script
   partway and leaving exactly the split population this PR is untangling.
   Behind auth and keyed per caller, 120 is as un-DoS-able as 30, and it
   matches inspectorRateLimit in the same file.

2. The same-file claim survived in three more files; comment-only fixes.

   agentsRuntime.ts:39 is the origin, and it is refuted 2000 lines below
   itself — /memory and /memory/sync follow the recipe exactly and both carry
   open high-severity alerts. install.ts:31 and provision.ts:41 inherited it,
   and are clean for a reason their comments do not state: both apply the
   limiter BEFORE auth. Each now records the real discriminator.

   agentsRuntime's routes are genuinely under-protected rather than
   false-positived, so the comment says so and says what fixing them requires
   (reorder plus an auth-independent key generator). Not doing it here:
   agentRateLimitKeyGenerator needs reading first, and that is a separate
   change rather than an oversight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(security): the agentsRuntime reorder is specified, not blocked

The comment added in a4d6935 said fixing those routes needs "a key
generator that does not depend on auth-set state" and deferred on reading
agentRateLimitKeyGenerator. I read it: no change is needed.

Its first branch uses req.agentTokenHash, which agentRuntimeAuth sets, but it
falls through to a sha256 of the Authorization / x-commonly-agent-token
header — present before any middleware runs. Moving the limiter ahead of auth
just takes the header branch: same per-caller isolation, different key prefix.

Correcting it because a comment naming a blocker that has since been checked
and cleared is the same defect this PR spent four commits removing from three
other files — a claim about the past that reads as current.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(memory): the cycle trailer names a tool moltbots do not have

The comment above withCyclesDirective asserted that the 2026-05-08 forward
fix added commonly_log_cycle to the openclaw extension. It did not. Inside
the running clawdbot-gateway the extension exposes 25 commonly_* tools and
grep -rl commonly_log_cycle /app returns nothing; the tool is defined only
in commonly-mcp/src/tools.js.

Measured consequence: every moltbot's last cycles append in agentmemories
is 83-87 days old, dating to when this trailer started naming the tool,
while MCP seats append hourly. The 17 agents carrying the trailer verbatim
are the control -- correct directive text is not sufficient when the tool
it names is absent from the runtime.

Comment-only. Records the evidence rather than the conclusion so the next
reader can falsify it against the same artifact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(memory): close the pin-skew loophole on the missing cycle tool

The prior comment cited the deployed gateway image only, which leaves the
obvious objection open: maybe the submodule pins a tree that has the tool
and the image is stale. It does not. _external/clawdbot pins openclaw
0082147920, and that ref's extensions/commonly/src/tools.ts has zero
occurrences of log_cycle against a post_message control of 2.

Pinned tree, repo tip (read by @ux-lead) and deployed image all agree.

Comment-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(memory): the cycle tool exists on the declared branch; the pin tracks another lineage

Correcting my own correction from e8a4368, which said 'not a pin skew, the
tool was never there'. It is there. .gitmodules declares
branch = rebase-2026.3.29 for _external/clawdbot; commonly_log_cycle landed on
that branch at a67f0df6 on 2026-05-09 -- the exact day this trailer started
naming it. The pin recorded on main is 0082147920, a different lineage, and
that is what builds the gateway.

So the original comment was TRUE when written and was invalidated underneath
by a pin move. Nothing about it had to change to become false, which is why it
survived 87 days.

Operational conclusion is unchanged: no live moltbot can call the tool.

Adds the warning the remedy needs -- bumping to the declared branch gains five
tools and LOSES react_to_message, so it owes a diff of both sets rather than a
version bump.

Found by @ux-lead. Comment-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: CLAUDE.md was wrong about the same tool block in both directions

Three entries described the openclaw extension's commonly_* block. Each named
a tool without naming a ref, so each was checkable only by someone willing to
exec into the gateway -- and none of them had been.

Verified against the RUNNING image, not the source tree
(/app/extensions/commonly/src/tools.ts, 38,086 bytes, byte-identical to the
pin; grepped with a positive control, because my first attempt pointed at
/app/dist and returned a clean-looking zero for every term including the
control):

  commonly_react_to_message   PRESENT, live handler   -- documented as absent
  commonly_open_dm            ABSENT                  -- documented as live
  commonly_log_cycle          ABSENT                  -- already corrected

Two authoritative claims about one 25-tool block, wrong in opposite
directions. Same root cause: .gitmodules declares branch = rebase-2026.3.29,
main records pin 0082147920, and nothing in the build reads that branch field.
The declared branch has the five memory/DM tools and lacks react_to_message;
the pin is the mirror image. They have disagreed since 2026-05-09.

So a pin bump is not a free fix -- it gains five tools and loses
react_to_message. New entry carries that table so the next reader does not
propose the bump as a one-liner. presets.ts carries the same table beside the
trailer.

Reactions: the moltbot/MCP split is real and the general rule stands, but
reactions are not an instance of it. The 2026-05-16 smoke that saw moltbots
post emoji as message content has not been re-run since the tool became
reachable, so behaviour stays unverified and the entry says so rather than
declaring the loop closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: the declared openclaw branch is 48 days STALE, not ahead — do not bump to it

Both artifacts said a pin bump "gains five tools and loses react_to_message,
so it owes a diff of both tool sets." That rule was scoped to the surface that
raised the question, and it is dangerous.

The pin IS openclaw main -- compare/main...0082147920 returns `identical`,
dated 2026-06-26. The branch .gitmodules declares heads at a67f0df6,
2026-05-09: 48 days OLDER, diverged, ahead 14 / behind 7. It is a stale fork,
not a forward target.

The 7 commits it is missing are load-bearing:

  fc6a2231  commonly_react_to_message
  2ce923b6  remove direct OAuth rotation from acpx_run, route via LiteLLM only
  78a6d174  treat acpx timeout as rate-limit so rotation triggers
  16a62bc4  honor OPENCLAW_INSTALL_GH_CLI to install the GitHub CLI
  eda5e1d4  install officecli + bake commonly-bundled-skills

So switching lineages reintroduces direct OAuth rotation inside acpx_run --
against the single-rotator invariant and the IP-bound-ChatGPT-session rule --
and breaks --build-arg OPENCLAW_INSTALL_GH_CLI=1, which CLAUDE.md's own
documented gateway build passes and the dev-agent GitHub PAT flow depends on.

A tool-set diff surfaces NONE of those. The rule I shipped 20 minutes ago
would have waved through both regressions. The check is a diff of the commit
RANGE.

Remedy corrected in both places: cherry-pick a67f0df6 (plus any of open_dm /
read_attachment / read_my_memory / save_my_memory still wanted) onto openclaw
main, then move the pin to that new main. Never point the submodule at the
branch.

Lineage facts from @ux-lead (52565 + the follow-up closing the deployed-image
question); the divergence and commit range verified here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(agents): the Docker provisioner never wrote the cycle trailer either

agentProvisionerServiceK8s.ts:489 wraps the default template in
withCyclesDirective; agentProvisionerService.ts did not. So an agent
provisioned on the Docker / self-host path with no matching preset got a
HEARTBEAT.md that structurally could not carry the directive -- the same
defect this PR fixes on the k8s path, on the sibling file, and the PR title
claims the default heartbeat path generally.

Scoped deliberately. ensureHeartbeatTemplate only writes the default when
HEARTBEAT.md is absent or effectively empty, so this repairs FRESH workspaces
only. The k8s path additionally treats "existing file missing
CYCLES_DIRECTIVE_MARKER" as stale and rewrites it; that clause is safe there
because skipHeartbeat short-circuits on customizations.heartbeat and
routes/registry/files.ts sets the flag for hand-authored files. This function
takes no customizations argument and its call site passes none, so porting the
clause would silently overwrite hand-edited files. Comment records the gap
rather than leaving it implied.

Left alone on purpose: writeOpenClawHeartbeatFileLocal writes caller-supplied
content (the hand-authored path), and must not have a directive injected into
what a human wrote.

Test asserts the file on disk, not the exported constant -- the defect was in
delivery, and pinning the constant would not have seen it. It clears its own
workspace because the suite's beforeEach clears the two config files but NOT
OPENCLAW_WORKSPACE_ROOT: a HEARTBEAT.md survives between runs, which is what
made the first version of this test fail against a file written 40 minutes
earlier. Mutation-checked green/red/green -- dropping withCyclesDirective reds
exactly this test, 13 others unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: porting the openclaw branch wholesale collides twice, one of them silently

The entry said "cherry-pick a67f0df6 ... onto main" but left a 14-commit port
open as an equally valid reading. It is not.

  branch 6c99dc31  tools.ts +11/-2   route acpx_run through LiteLLM via opencode
  main   2ce923b6  tools.ts  +9/-73  remove direct OAuth rotation from acpx_run

Main deleted 73 lines; the branch added to that region nine days later, solving
the same problem differently. Porting re-introduces what main removed.

  branch 8b50281b  +125 tools.ts +43 client.ts +9 src/plugin-sdk/index.ts
  main   00821479   +22 tools.ts +68 client.ts

commonly_attach_file exists on BOTH lineages as independent implementations.
A wholesale port duplicates the registration, in different regions of different
files, so git may not conflict at all -- the failure surfaces at runtime, not
in review.

By contrast a67f0df6 touches one file, +36/-0, pure addition. It cannot collide.

Also records why this survived four months: a submodule bump never touches
.gitmodules. `git -C _external/clawdbot checkout <sha> && git add
_external/clawdbot` leaves the declaration out of the diff, the command and the
review. The pin was deliberately moved as recently as 2026-06-26 to gain
commonly_attach_file, by someone with no reason to open the file contradicting
them. Not neglect -- a field positioned to look like configuration in a
workflow that cannot surface it.

Lineage collisions raised as open questions by @ux-lead; diffstats read here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: the pin doesn't sit still — it ALTERNATES between the two lineages

Fourth rewrite of this comment in one afternoon, and the reason is the finding:
the pin is not a stale pointer nobody moved. It has moved 15+ times and has
crossed lineages repeatedly.

  2026-05-09  f4b7a48  a67f0df6  BRANCH   log_cycle ARRIVES
  2026-05-17  b6a811b  fc6a2231  main     LOST      (bump was for react_to_message)
  2026-05-21  0168f01  a67f0df6  BRANCH   RESTORED  (#418, explicitly)
  2026-05-24  d6e63b2  84549161  main     LOST      (bump was for bundled-skills)
  2026-06-26  a3de6d0  00821479  main     current

Three corrections to what this file said an hour ago:

- not "never pinned" -- pinned twice, and it worked twice
- not "nobody looked at the gitlink" -- #418's subject is literally
  `bump _external/clawdbot fc6a22319 -> a67f0df63`. Somebody caught this exact
  regression on 05-21. A bundled-skills bump undid it three days later.
- not "a stale fork to avoid" -- the branch was a deliberate target twice

Nobody was negligent. A submodule bump surfaces the tool it was made for and
says nothing about the five it trades away; the diff is one line of hex.

Cross-validated by @ux-lead against per-agent last-cycles-append timestamps:
writes cluster at 05-09..05-13 and 05-21..05-23, both strictly inside a
branch-pinned window, nothing outside them. Mongo and the submodule log agree
to the day.

Remedy restated: not a bump in either direction, but ending the divergence.
Anything less leaves the next unrelated bump free to swap the set back --
which is what happened twice after #418 had already fixed it.

Gitlink history read here from `git ls-tree` at each commit that touched
_external/clawdbot; windows and cycles correlation from @ux-lead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(agents): a check that reads the openclaw ref, instead of a fifth paragraph

The comment in presets.ts describing the openclaw lineage skew was rewritten
FOUR times in one afternoon, each version confidently wrong in a different
direction. Not carelessness at any one step -- every version was verified
before it shipped. A claim about another repo's state does not get fixed; it
decays, and prose has no mechanism to notice.

scripts/verify-moltbot-tool-contract.js resolves the pinned submodule tree,
parses the tool DECLARATIONS out of extensions/commonly/src/tools.ts, and
fails when a tool the cycles trailer instructs moltbots to call is not among
them. Required tools are derived from CYCLES_REFLECTION_TRAILER itself rather
than restated, so editing the trailer to name a different tool is covered
without touching the script.

Would have fired on 2026-05-17 and again on 2026-05-24 -- the two unrelated
bumps that swapped the lineage back after #418 had fixed it by hand.

Design notes that are load-bearing:

- Declarations, not mentions. `name: "commonly_x"` counts; the string
  appearing in a description does not. The original defect was a name in
  prose asserting a capability, so a parser that accepts prose reproduces
  that defect inside the guard against it. Mutation-checked: loosening the
  regex reds exactly the prose test.
- Exit 2 for "cannot verify" (submodule absent, or zero declarations parsed
  from a non-empty file), never 0. Four instruments returned clean zeros
  today whose controls also returned zero; an unrun check must not look like
  a passing one.
- Scoped to the trailer, not the agentMentionService cues, because #818 is
  changing those and a guard straddling an open PR is a merge conflict rather
  than a safeguard. Widening point documented in the header.

NOT WIRED TO A WORKFLOW YET, deliberately. Only deploy-dev.yml checks out
submodules, and the check fails there today because the regression is live --
so wiring it now means either a blocked deploy or a non-blocking check, and a
check that cannot fail is exactly the decorative-config defect this whole
investigation is about, one layer up. It belongs in the reconciliation PR,
where it goes green the moment it goes live.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(adr): five ADR claims about the openclaw extension, all wrong at the pin

Verified against the RUNNING gateway, not the source tree
(/app/extensions/commonly/src/tools.ts, 25 declared tools):

  commonly_log_cycle          0    ADR-010:125, ADR-012:408, ADR-012:504
  commonly_open_dm            0    ADR-013:750
  commonly_read_my_memory     0    ADR-003:178
  commonly_save_my_memory     0    ADR-003:178
  commonly_read_agent_memory  1    <- control, and the correction below
  commonly_write_agent_memory 1

ADR-003:178 is backwards rather than merely wrong: the two tools it calls
primary are absent and the two it describes as "v1-compatible wrappers
retained" are the only memory tools the shipped extension has.

ADR-012:504 is the one worth reading. It is not a documentation error -- it
was TRUE when written. commonly#307 (f4b7a48, 2026-05-09) really did pin
a67f0df6, and moltbots really did log cycles. Then the pin ALTERNATED:

  2026-05-09  f4b7a48  a67f0df6  BRANCH   ARRIVES   <- commonly#307
  2026-05-17  b6a811b  fc6a2231  main     LOST      (bump was for react_to_message)
  2026-05-21  0168f01  a67f0df6  BRANCH   RESTORED  (commonly#418)
  2026-05-24  d6e63b2  84549161  main     LOST      (bump was for bundled-skills)
  2026-06-26  a3de6d0  00821479  main     current

Three authors adding three unrelated features, each silently trading away five
tools, in a diff that is one line of hex and names none of them. So the fix
for these five is not better proofreading -- the claims decayed rather than
being written wrong, and nothing in review could see it. That is what
scripts/verify-moltbot-tool-contract.js exists for; each correction points at
it.

Claim locations surfaced by @ux-lead; every one re-verified here against the
live image before editing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(ci): the smoke gate's clawdbot glob has never matched a submodule bump

ADR-009 Phase 2 added `_external/clawdbot/**` to the smoke gate's
pull_request paths so a gateway bump would get the kind smoke before
merging. It cannot match. A submodule bump changes the gitlink entry
itself, and GitHub reports that changed path as exactly
`_external/clawdbot` — nothing is ever under it, because the parent repo
tracks no files inside the submodule.

Verified against the two gitlink-only PRs rather than by reasoning about
glob semantics: #418 and #443 both report `files: ["_external/clawdbot"]`
from the PR files API, and neither PR head has a `kind cluster smoke
test` check run. Both DID get smoked after merge, via the `workflow_run`
trigger on main — so this is not "never ran", it is "never ran while it
could still block the merge", which is the entire purpose of a PR gate.

That matters here specifically: the clawdbot-gateway image is built from
this submodule, five bumps have landed, and three of them silently
swapped the extension's whole `commonly_*` tool set between two diverged
openclaw lineages.

Keep both entries. The bare path catches the bump; the glob still covers
vendored files if any are ever tracked directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant