Skip to content

Commit fe98cbe

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/plugin-sdk-raw-send-failures
2 parents 4996725 + b2e34b0 commit fe98cbe

62 files changed

Lines changed: 3049 additions & 2024 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/setup-node-env/action.yml

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ inputs:
6666
save-vitest-fs-cache:
6767
description: >
6868
Whether this job may save the shared Vitest filesystem module cache.
69-
Other matrix jobs restore the warm seed without committing competing
70-
snapshots.
69+
Only honored outside pull_request events for sticky disks; PR jobs
70+
always mount the protected snapshot read-only.
7171
required: false
7272
default: "false"
7373
build-all-cache-scope:
@@ -144,27 +144,24 @@ runs:
144144
# cannot seed this key.
145145
commit: ${{ inputs.save-sticky-disk == 'true' && github.event_name != 'pull_request' && 'true' || 'false' }}
146146

147-
- name: Mount protected Vitest transform seed
148-
if: inputs.vitest-fs-cache == 'true' && (inputs.sticky-disk == 'true' || inputs.runtime-cache-sticky-disk == 'true') && github.event_name == 'pull_request' && runner.os != 'Windows'
149-
uses: useblacksmith/stickydisk@5b350170ae4ef55b536b548ef5f5896e76a6b54f # v1.4.0
150-
with:
151-
# Same-repository PRs read the protected snapshot but never write it.
152-
# Content-addressed misses stay in the PR-scoped overlay below.
153-
key: ${{ github.repository }}-vitest-fs-v2-protected-${{ runner.os }}-${{ runner.arch }}-node-${{ inputs.node-version }}
154-
path: /var/tmp/openclaw-vitest-fs-protected-seed
155-
commit: false
156-
157147
- name: Mount Vitest transform cache sticky disk
158148
if: inputs.vitest-fs-cache == 'true' && (inputs.sticky-disk == 'true' || inputs.runtime-cache-sticky-disk == 'true') && runner.os != 'Windows'
159149
uses: useblacksmith/stickydisk@5b350170ae4ef55b536b548ef5f5896e76a6b54f # v1.4.0
160150
with:
161-
# One stable disk per trust scope. The generation guard below clears
151+
# One stable disk per runner shape. The old per-PR overlay disks
152+
# minted a new backing disk for every PR and helped saturate
153+
# Blacksmith's installation-wide sticky-disk budget, 429-failing every
154+
# mount. Cache entries are content-hash keyed (id+content+env), so PRs
155+
# can safely read the protected snapshot directly; PR-local misses are
156+
# rebuilt in the discarded mount. The generation guard below clears
162157
# incompatible transform inputs without leaking one disk per lockfile.
163-
key: ${{ github.repository }}-vitest-fs-v2-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'protected' }}-${{ runner.os }}-${{ runner.arch }}-node-${{ inputs.node-version }}
158+
key: ${{ github.repository }}-vitest-fs-v2-protected-${{ runner.os }}-${{ runner.arch }}-node-${{ inputs.node-version }}
164159
path: /var/tmp/openclaw-vitest-fs-cache
165-
# One semantic writer only. Explicit true avoids stickydisk's allocated-
166-
# byte heuristic missing equal-size replacements or prune-balanced writes.
167-
commit: ${{ inputs.save-vitest-fs-cache == 'true' && 'true' || 'false' }}
160+
# Single semantic writer: only the designated non-PR writer commits;
161+
# pull_request mounts stay read-only clones. Explicit true avoids
162+
# stickydisk's allocated-byte heuristic missing equal-size
163+
# replacements or prune-balanced writes.
164+
commit: ${{ inputs.save-vitest-fs-cache == 'true' && github.event_name != 'pull_request' && 'true' || 'false' }}
168165

169166
- name: Restore and save Vitest transform cache
170167
if: inputs.vitest-fs-cache == 'true' && inputs.sticky-disk != 'true' && inputs.runtime-cache-sticky-disk != 'true' && inputs.save-vitest-fs-cache == 'true' && runner.os != 'Windows'
@@ -190,30 +187,28 @@ runs:
190187
if: inputs.vitest-fs-cache == 'true' && runner.os != 'Windows'
191188
env:
192189
CACHE_GENERATION: ${{ hashFiles('pnpm-lock.yaml', 'pnpm-workspace.yaml', '**/package.json', '**/tsconfig*.json', 'vitest.config.*', 'test/vitest/**') }}
193-
CACHE_WRITER: ${{ inputs.save-vitest-fs-cache == 'true' && '1' || '0' }}
190+
# Sticky PR mounts never commit, so pruning there would only burn
191+
# shard wall clock on a discarded clone; non-sticky (actions/cache)
192+
# writers still prune their PR-scoped archives.
193+
CACHE_WRITER: ${{ inputs.save-vitest-fs-cache == 'true' && ((inputs.sticky-disk != 'true' && inputs.runtime-cache-sticky-disk != 'true') || github.event_name != 'pull_request') && '1' || '0' }}
194194
shell: bash
195195
run: |
196196
set -euo pipefail
197197
cache_root=/var/tmp/openclaw-vitest-fs-cache
198-
seed_root=/var/tmp/openclaw-vitest-fs-protected-seed
199198
generation_file="$cache_root/.openclaw-transform-generation"
200199
mkdir -p "$cache_root"
201200
cache_generation=""
202201
if [[ -f "$generation_file" ]]; then
203202
cache_generation="$(<"$generation_file")"
204203
fi
205204
cache_entry="$(find "$cache_root" -mindepth 1 -maxdepth 1 -print -quit)"
205+
# Read-only PR mounts clear a mismatched generation too: the wipe only
206+
# affects the discarded local clone, and mixing generations could
207+
# false-hit transforms produced under incompatible tsconfig/lockfile.
206208
if [[ -n "$cache_entry" ]] && [[ "$cache_generation" != "$CACHE_GENERATION" ]]; then
207209
echo "Vitest transform inputs changed; clearing incompatible cache generation"
208210
find "$cache_root" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +
209211
fi
210-
seed_generation_file="$seed_root/.openclaw-transform-generation"
211-
if [[ -f "$seed_generation_file" ]] && [[ "$(<"$seed_generation_file")" == "$CACHE_GENERATION" ]]; then
212-
echo "Merging matching protected Vitest transform seed"
213-
cp -a --no-clobber "$seed_root"/. "$cache_root"/
214-
elif [[ -f "$seed_generation_file" ]]; then
215-
echo "Ignoring protected Vitest transform seed from an incompatible generation"
216-
fi
217212
printf '%s\n' "$CACHE_GENERATION" > "$generation_file"
218213
# The shard runner treats this as a persistent root and assigns one
219214
# isolated subdirectory per concurrent Vitest worker.

.github/workflows/ci.yml

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,8 +1833,9 @@ jobs:
18331833
vitest-fs-cache: "true"
18341834
node-compile-cache: "true"
18351835
node-compile-cache-scope: "test"
1836-
# One matrix job commits the shared warm seed. Other jobs restore it
1837-
# read-only, avoiding concurrent sticky-disk or archive writers.
1836+
# One matrix job is the designated warm-seed writer; the action only
1837+
# honors it outside pull_request events, so PR shards always read the
1838+
# protected snapshot without committing competing writes.
18381839
save-vitest-fs-cache: ${{ matrix.save_vitest_fs_cache && 'true' || 'false' }}
18391840
save-node-compile-cache: ${{ matrix.save_vitest_fs_cache && 'true' || 'false' }}
18401841

@@ -3135,28 +3136,54 @@ jobs:
31353136
if: github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw')
31363137
uses: useblacksmith/stickydisk@5b350170ae4ef55b536b548ef5f5896e76a6b54f # v1.4.0
31373138
with:
3138-
# Task-scoped keys keep one semantic writer per snapshot: each matrix
3139-
# task resolves a different dependency set, and a shared if-missing
3140-
# key would let a light task (ktlint) win the cold-key race and pin a
3141-
# thin snapshot the heavy build lanes can never repair. Gradle's own
3142-
# caches are content-addressed, so stale restored entries are ignored
3143-
# and dependency-file changes mint a fresh key/snapshot.
3144-
key: ${{ github.repository }}-gradle-v1-${{ matrix.task }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || 'protected' }}-${{ hashFiles('apps/android/**/*.gradle*', 'apps/android/**/gradle-wrapper.properties', 'apps/android/gradle/libs.versions.toml') }}
3139+
# One stable disk per matrix task. The v1 per-PR/per-dependency-hash
3140+
# keys minted a new backing disk for every PR and dependency bump
3141+
# and helped saturate Blacksmith's installation-wide sticky-disk
3142+
# budget, 429-failing every mount. Task scope stays in the key: each
3143+
# matrix task resolves a different dependency set, and a shared key
3144+
# would let a light task (ktlint) pin a thin snapshot the heavy
3145+
# build lanes can never repair. Dependency-file changes live in the
3146+
# runtime fingerprint marker below instead of the key.
3147+
key: ${{ github.repository }}-gradle-v2-${{ matrix.task }}
31453148
path: /var/tmp/openclaw-gradle
3146-
# on-change keeps the snapshot learning: Gradle's build cache
3147-
# accumulates across runs (if-missing would freeze the first
3148-
# hydration forever). Entries are content-addressed, task-scoped
3149-
# keys give one semantic writer, and v1.4.0 still skips commit
3150-
# after failed/cancelled steps.
3151-
commit: on-change
3149+
# Single semantic writer: only protected pushes commit, so
3150+
# pull_request clones stay read-only and the snapshot tracks main.
3151+
# Explicit true (not on-change) because the allocated-byte heuristic
3152+
# can miss a same-size refresh and strand the fingerprint marker;
3153+
# v1.4.0 still skips commit after failed/cancelled steps, so a
3154+
# broken build cannot poison this key.
3155+
commit: ${{ github.event_name != 'pull_request' && 'true' || 'false' }}
31523156

31533157
- name: Point Gradle at the sticky disk
31543158
if: github.event_name != 'workflow_dispatch' && github.repository == 'openclaw/openclaw' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'openclaw/openclaw')
31553159
shell: bash
3160+
env:
3161+
# Gradle caches are content-addressed, so a stale snapshot is safe
3162+
# to restore (obsolete entries are simply ignored). The fingerprint
3163+
# only bounds disk growth: when dependency inputs change, the writer
3164+
# rebuilds its snapshot from scratch so retired dependency artifacts
3165+
# do not accumulate on the O(1) key forever.
3166+
GRADLE_DEPS_FINGERPRINT: ${{ hashFiles('apps/android/**/*.gradle*', 'apps/android/**/gradle-wrapper.properties', 'apps/android/gradle/libs.versions.toml') }}
3167+
STICKY_WRITER: ${{ github.event_name != 'pull_request' && 'true' || 'false' }}
31563168
run: |
31573169
set -euo pipefail
3158-
mkdir -p /var/tmp/openclaw-gradle/gradle-user-home
3159-
echo "GRADLE_USER_HOME=/var/tmp/openclaw-gradle/gradle-user-home" >> "$GITHUB_ENV"
3170+
sticky_root=/var/tmp/openclaw-gradle
3171+
marker="$sticky_root/.openclaw-gradle-deps-fingerprint"
3172+
disk_fingerprint=""
3173+
if [ -f "$marker" ]; then
3174+
disk_fingerprint="$(<"$marker")"
3175+
fi
3176+
if [ "$disk_fingerprint" != "$GRADLE_DEPS_FINGERPRINT" ]; then
3177+
if [ "$STICKY_WRITER" = "true" ]; then
3178+
echo "Gradle dependency inputs changed; rebuilding snapshot cold to drop retired artifacts"
3179+
rm -rf "$sticky_root/gradle-user-home"
3180+
printf '%s\n' "$GRADLE_DEPS_FINGERPRINT" > "$marker"
3181+
else
3182+
echo "Gradle dependency inputs changed since snapshot; content-addressed caches stay safe to reuse"
3183+
fi
3184+
fi
3185+
mkdir -p "$sticky_root/gradle-user-home"
3186+
echo "GRADLE_USER_HOME=$sticky_root/gradle-user-home" >> "$GITHUB_ENV"
31603187
31613188
- name: Run Android ${{ matrix.task }}
31623189
working-directory: apps/android

.github/workflows/pr-cache-cleanup.yml

Lines changed: 0 additions & 21 deletions
This file was deleted.

config/max-lines-baseline.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,6 @@ extensions/slack/src/approval-native.test.ts
278278
extensions/slack/src/channel.test.ts
279279
extensions/slack/src/channel.ts
280280
extensions/slack/src/message-action-dispatch.test.ts
281-
extensions/slack/src/monitor/context.ts
282281
extensions/slack/src/monitor/events/interactions.block-actions.ts
283282
extensions/slack/src/monitor/events/interactions.test.ts
284283
extensions/slack/src/monitor/media.test.ts
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
6aed79bb0351969499e9b40d633e95fea5bf44bf234dda2b9b6d73111a96867d config-baseline.json
1+
07edb0e3c7841952ccfd5111b4be02db9c7c52ff8fc4bb185dc87552f7776116 config-baseline.json
22
337632b33a9e0c27c5e4f1ed8229216e9a0a54d3f277d95d0782840f4e1ae4f9 config-baseline.core.json
3-
52702a2f7628ea48bf6abc610bea6883c40212de11b48daf0eabeb1306c3dd46 config-baseline.channel.json
4-
f2cf8f2902c8ad988534579448c28a0f8fc8252110f8c03bc34bf0cb9448224c config-baseline.plugin.json
3+
911a0a6bedaeebda7c92ea1ccfaedb223f23e96bf577398601ec61e34d35ed08 config-baseline.channel.json
4+
5ccd3695d1c1a60efa2654e0fd9a0397ef9c45fd1e1fd30a3cbed9b343a5c8ca config-baseline.plugin.json
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
8b832e3973b7530eb65ebf7731d45f8d7c4d57b68065dd7c74912db4502eedbc plugin-sdk-api-baseline.json
2-
9a1adc5fbff805b5b8b1af25f569b7b2dbdde505a550072621ba569fd32fdd1d plugin-sdk-api-baseline.jsonl
1+
244b02936e24b4c5ae8ca596c7a2255e2da1026b23ad445108bd5135d6ed8564 plugin-sdk-api-baseline.json
2+
3566fb622a7173c4e238d449d2c605a43a21cae6d966782acb046a7925d3a20e plugin-sdk-api-baseline.jsonl

docs/channels/bot-loop-protection.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ The guard does not affect human-authored messages, single-bot deployments, self-
2626

2727
## Configure shared defaults
2828

29-
Set `channels.defaults.botLoopProtection` once to give every supporting channel the same baseline. Channel, account, and room overrides can still tune individual surfaces.
29+
Set `channels.defaults.botLoopProtection` once to give every supporting channel the same baseline. Channels may also expose narrower overrides; Feishu intentionally uses only this shared baseline.
3030

3131
```json5
3232
{
@@ -68,7 +68,7 @@ Supporting channels layer their own config over the shared default, key by key.
6868
},
6969
accounts: {
7070
secondary: {
71-
allowBots: "mentions",
71+
allowBots: true,
7272
botLoopProtection: {
7373
maxEventsPerWindow: 5,
7474
cooldownSeconds: 90,
@@ -109,6 +109,7 @@ Supporting channels layer their own config over the shared default, key by key.
109109
## Channel support
110110

111111
- Discord: native `author.bot` facts, keyed by Discord account, channel, and bot pair.
112+
- Feishu: native `sender_type=bot` facts for admitted bot-authored group messages, keyed by Feishu account, chat, and bot pair. Feishu uses only `channels.defaults.botLoopProtection`.
112113
- Google Chat: native `sender.type=BOT` facts for accepted bot-authored messages, keyed by account, space, and bot pair.
113114
- Matrix: configured Matrix bot accounts, keyed by Matrix account, room, and configured bot pair.
114115
- Slack: native `bot_id` facts for accepted bot-authored messages, keyed by Slack account, channel, and bot pair.

docs/channels/feishu.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,22 @@ In `allowlist` mode, you can also admit a group by adding an explicit `groups.<c
150150

151151
`channels.feishu.groupSenderAllowFrom` sets the same sender allowlist for all groups; a per-group `allowFrom` takes precedence.
152152

153+
### Bot-authored messages
154+
155+
Feishu ignores messages authored by other bots by default. To allow bot-to-bot group conversations, grant the app the `im:message.group_at_msg.include_bot:readonly` and `im:message:readonly` scopes, then set `allowBots`:
156+
157+
```json5
158+
{
159+
channels: {
160+
feishu: {
161+
allowBots: true,
162+
},
163+
},
164+
}
165+
```
166+
167+
Feishu only delivers bot-authored group events when another bot mentions this bot. Existing group policy, sender allowlists, and mention requirements still apply. OpenClaw drops self-authored messages, mentions the peer bot on every text or card reply, and applies the shared [`channels.defaults.botLoopProtection`](/channels/bot-loop-protection) guard.
168+
153169
<a id="get-groupuser-ids"></a>
154170

155171
## Get group/user IDs

docs/docs_map.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
376376
- H3: Allow all groups, still require @mention
377377
- H3: Allow specific groups only
378378
- H3: Restrict senders within a group
379+
- H3: Bot-authored messages
379380
- H2: Get group/user IDs
380381
- H3: Group IDs (chatid, format: ocxxx)
381382
- H3: User IDs (openid, format: ouxxx)

extensions/feishu/openclaw.plugin.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@
205205
"type": "string",
206206
"enum": ["disabled", "enabled"]
207207
},
208+
"allowBots": { "type": "boolean" },
208209
"typingIndicator": { "type": "boolean" }
209210
}
210211
}
@@ -241,6 +242,7 @@
241242
"type": "string",
242243
"enum": ["disabled", "enabled"]
243244
},
245+
"allowBots": { "type": "boolean" },
244246
"typingIndicator": { "type": "boolean" },
245247
"accounts": {
246248
"type": "object",

0 commit comments

Comments
 (0)