Repository navigation
fix(feishu): inbound files dropped on captioned or multi-file posts - #151896
Conversation
|
🦞👀 Pull request received. I will update this pull request when review starts. ClawSweeper review completeClawSweeper finished reviewing this revision. The review result is being finalized. |
|
Codex review: needs maintainer review before merge. Reviewed October 1, 2026, 12:25 AM ET / 04:25 UTC (Revision 13). ClawSweeper reviewWhat this changesThe Feishu plugin downloads files from captioned and multi-file rich-text posts while preserving inline-media classification and existing replay identities. Merge readiness✅ Ready for maintainer review The fix remains necessary on current main and the latest release. Prior blockers are resolved, the recorded Gateway proof covers the repaired attachment path, and no new actionable defect was found. Priority: P1 Review scores
Verification
How this fits togetherThe Feishu plugin turns incoming channel messages into text and saved attachments for the agent. Its replay guard prevents previously processed messages from starting another turn. flowchart TD
A[Incoming Feishu post] --> B[Replay guard]
B -->|Previously processed| C[Suppress duplicate]
B -->|New message| D[Parse caption and attachments]
D --> E[Download message resources]
E --> F[Save attachments]
F --> G[Agent message context]
Before mergeNone. Agent review detailsSecurityNone. PR surfaceSource +60, Tests +479, Docs +4. Total +543 across 11 files. View PR surface stats
Review metrics
Technical reviewBest possible solution: Keep attachment collection in the existing Feishu parser and download owner while retaining the shipped replay projection and inline-video semantics. Do we have a high-confidence way to reproduce the issue? Yes: the reported post payloads contain top-level files that current main never collects, and the recorded Gateway baseline confirms zero attachments. This read-only review did not independently execute the reproduction. Is this the best way to solve the issue? Yes: extending the existing parser and resource path repairs the missing payload shape without a competing owner, while both replay consumers preserve existing identities. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning medium; reviewed against d7b029347fd0. LabelsLabel changes:
Label justifications:
EvidenceWhat I checked:
Likely related people:
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (12 earlier review cycles; latest 8 shown)
|
8219538 to
6e3ce99
Compare
|
Replay-key P1 is addressed on this head: captioned-post |
1026276 to
7339990
Compare
|
Unnamed top-level files[] were landing as video even when Feishu later handed back a PDF name/type. Parser now keeps those as documents; unnamed inline media still falls back to video. |
Fixes openclaw#151792 Signed-off-by: GokayAI <[email protected]>
Preserve shipped post replay keys so a pre-upgrade captioned post persisted as om_post still suppresses redelivery after top-level files[] are downloaded. Signed-off-by: GokayAI <[email protected]>
Move captioned/multi-file handleFeishuMessage coverage into a sibling file so bot.test.ts does not grow past the line-cap ratchet. Type the media saver mock so mock.calls mapping typechecks. Signed-off-by: GokayAI <[email protected]>
Top-level files[] without file_name were classified as video, so a download-supplied PDF name and MIME still skipped file extraction. Carry document origin from the parser and keep the inline-media fallback. Signed-off-by: GokayAI <[email protected]>
Use the live mocked saver export and reset Feishu dedupe so captioned and multi-file handleFeishuMessage coverage still records downloads in the shared extension worker. Give mockCallArg a type-predicate slot so oxlint no-unnecessary-type-parameters passes. Signed-off-by: GokayAI <[email protected]>
Route bot.post-files.test.ts with bot.test.ts onto isolated database-worker forks so shared extension-feishu mocks cannot steal media.js. Assert and stub the live saveMessageResourceFeishu export resolveFeishuMediaList calls. Signed-off-by: GokayAI <[email protected]>
8151735 to
bc1cb02
Compare
|
@clawsweeper re-review |
|
🦞👀 Re-review progress:
|
|
@clawsweeper re-review |
|
🦞👀 Re-review progress:
|
|
@clawsweeper re-review |
|
🦞👀 Re-review progress:
|
|
Merged — thank you @gokay-ai for the original fix, and @WhitenessTiger for the report and document-classification diagnosis. Captioned Feishu post files now reach agent context through the existing download path, with byte-level Gateway proof and retained replay compatibility. |
…penclaw#151896) Feishu `post` messages that carried top-level `files[]` attachments only passed the caption to the agent: the files were never downloaded. They're now downloaded and attached to the agent context, and the legacy replay of stored records projects them the same way. Fixes openclaw#151792. Thanks @gokay-ai for the fix, and @Leon-SK668 for the alternative in openclaw#153934 and its byte-level proof. Proof: a real isolated Gateway with a signed Feishu webhook and a local HTTPS API. - On main, the agent received only the caption. With this change, it receives the caption plus a byte-identical CSV document. - An attachment-free post is unchanged. - A seeded legacy-record replay through the actual monitor fails before the fix and passes after. Co-authored-by: Ayaan Zaidi <[email protected]>
…penclaw#151896) Feishu `post` messages that carried top-level `files[]` attachments only passed the caption to the agent: the files were never downloaded. They're now downloaded and attached to the agent context, and the legacy replay of stored records projects them the same way. Fixes openclaw#151792. Thanks @gokay-ai for the fix, and @Leon-SK668 for the alternative in openclaw#153934 and its byte-level proof. Proof: a real isolated Gateway with a signed Feishu webhook and a local HTTPS API. - On main, the agent received only the caption. With this change, it receives the caption plus a byte-identical CSV document. - An attachment-free post is unchanged. - A seeded legacy-record replay through the actual monitor fails before the fix and passes after. Co-authored-by: Ayaan Zaidi <[email protected]> (cherry picked from commit 7b7d195)
…e commit (#176065) Related: #176053 (merged; it trims root `AGENTS.md` under the cap) ## What Problem This Solves Fixes: PRs fail CI on their first push for generated files, docs lint and size caps that a seconds-fast local check would have caught, and root `AGENTS.md` regrows past the 20,000-character cap that OpenClaw loads whole. ## User Impact No product change. Contributors and agents see these failures at commit time with the regen command, not after a CI round trip. A tracked `AGENTS.md` over the workspace bootstrap cap now fails CI. ## Why This Change Was Made - **Pre-commit freshness checks.** `git-hooks/pre-commit` (installed by `pnpm install`) now runs existing `--check` scripts, chosen from staged paths and run in parallel: `lint:docs` on changed docs, `protocol:check`, `native:i18n:verify`, `config:docs:check`, the line-cap ratchet, and the new AGENTS.md size check. Each failure prints its fix command. Typecheck, lint and tests stay with hosted CI, and commits replayed by merge, rebase, cherry-pick or revert skip the checks. The content guard (`guard-staged-content.mjs`) runs the step and redacts its output with its existing redactor, so blocked private literals are never echoed. The hook judges only the bytes being committed: checks that import the working-tree code graph (protocol, native i18n, config docs, AGENTS.md size) are skipped with a one-line note when any tracked file has unstaged edits, `lint:docs` is skipped only when its own files or config are dirty, and the line-cap ratchet reads the index and always runs. Plain `node` package scripts start without pnpm. Path predicates live in their owners: `changed-lanes.mts` (also used by `check:changed` for the line-cap ratchet) and `hasProtocolGenerationInput` shared with CI's check family scope. - **AGENTS.md size check.** `scripts/check-agents-md-size.mts` reads the cap from `resolveBootstrapMaxChars()` instead of copying the number. It runs in the hook, `pnpm check` and the CI check-docs job. Left out on purpose, because each is too slow or already red on `main` for unrelated files: max-lines ratchet (~30 s), knip unused files (92 s), madge cycles (22 s), Control UI i18n (16 s), plugin SDK API diff (two full builds). ## Evidence Back-test against first-push CI failures from a 90-day sample of merged PRs (26 rows; 2 retired checks; detached worktrees at each failing head): | Row | CI failure | Hook before | Hook after | |---|---|---|---| | #122960 | docs MD051 link fragments | pass | **fail**, same 3 errors as CI | | #112339 | Swift protocol models drift | pass | **fail**, same drift | | #115577 | native i18n inventory drift | pass | **fail**, same message | | #165543 | config baseline hash | pass | **fail**, same hash mismatch | | #151896 | line-cap growth 3557 → 3676 | pass | **fail**, same counts | The other 19 live rows need typecheck, tests, UI build or slow audits, which stay in hosted CI by design. Size check back-test: it first fails at bb7d32e (#167739, 20,414 chars), and fails again at b19d554 (#173903) after the earlier trim. Freshness step wall time, median of 3 on a warm checkout (before = previous head 7aae979, after = this head): | Staged change | Checks | Before | After | |---|---|---|---| | nothing | none | 0.17 s | 0.28 s | | `CHANGELOG.md` | none | 0.20 s | 0.24 s | | one docs page | lint:docs | 0.92 s | 1.13 s | | one `src/**/*.ts` | line-cap | 1.29 s | 1.42 s | | a scoped `AGENTS.md` | size check | 0.92 s | 1.07 s | | protocol schema `.ts` | protocol, line-cap | 4.77 s | 4.39 s | | Android Kotlin source | protocol, native i18n | 4.41 s | 4.48 s | | plugin manifest | config docs | 8.19 s | 8.97 s | Spawning `node` directly saves about 0.1–0.3 s per check against `pnpm -s`; the after column also pays one extra `git diff` for the unstaged-input rule. Most of the remaining time is in the checks themselves. `protocol:check` and `config:docs:check` are not cached: their inputs are transitive imports across `src/`, `packages/` and every plugin manifest, and no existing owner lists them reliably. Checks run: new and existing hook tests, including `test/git-hooks-pre-commit-freshness.test.ts` (real guard, real freshness step and a real `node` runner: a partly staged docs page with a blocked literal in its unstaged edit is skipped; a staged protocol schema with an unstaged edit in an imported owner outside the trigger list skips `protocol:check` while the line-cap ratchet still runs; and a failing check that prints a private literal is redacted), `test/scripts/check-staged-freshness.test.ts`, `changed-lanes.test.ts`, `check.test.ts`, `tsgo:scripts`, `tsgo:test:root`, oxlint, oxfmt. All commits went through the new hook. **Merge danger:** two-way door; a revert restores the previous hook and CI step. No state, config or published contract changes. Blast radius: every local commit in this repository (adds about 0.3 s to 9 s depending on staged paths) and the CI check-docs job. #176053 has landed, so the size check passes on `main`. Co-authored-by: Ayaan Zaidi <[email protected]>
…e commit (openclaw#176065) Related: openclaw#176053 (merged; it trims root `AGENTS.md` under the cap) ## What Problem This Solves Fixes: PRs fail CI on their first push for generated files, docs lint and size caps that a seconds-fast local check would have caught, and root `AGENTS.md` regrows past the 20,000-character cap that OpenClaw loads whole. ## User Impact No product change. Contributors and agents see these failures at commit time with the regen command, not after a CI round trip. A tracked `AGENTS.md` over the workspace bootstrap cap now fails CI. ## Why This Change Was Made - **Pre-commit freshness checks.** `git-hooks/pre-commit` (installed by `pnpm install`) now runs existing `--check` scripts, chosen from staged paths and run in parallel: `lint:docs` on changed docs, `protocol:check`, `native:i18n:verify`, `config:docs:check`, the line-cap ratchet, and the new AGENTS.md size check. Each failure prints its fix command. Typecheck, lint and tests stay with hosted CI, and commits replayed by merge, rebase, cherry-pick or revert skip the checks. The content guard (`guard-staged-content.mjs`) runs the step and redacts its output with its existing redactor, so blocked private literals are never echoed. The hook judges only the bytes being committed: checks that import the working-tree code graph (protocol, native i18n, config docs, AGENTS.md size) are skipped with a one-line note when any tracked file has unstaged edits, `lint:docs` is skipped only when its own files or config are dirty, and the line-cap ratchet reads the index and always runs. Plain `node` package scripts start without pnpm. Path predicates live in their owners: `changed-lanes.mts` (also used by `check:changed` for the line-cap ratchet) and `hasProtocolGenerationInput` shared with CI's check family scope. - **AGENTS.md size check.** `scripts/check-agents-md-size.mts` reads the cap from `resolveBootstrapMaxChars()` instead of copying the number. It runs in the hook, `pnpm check` and the CI check-docs job. Left out on purpose, because each is too slow or already red on `main` for unrelated files: max-lines ratchet (~30 s), knip unused files (92 s), madge cycles (22 s), Control UI i18n (16 s), plugin SDK API diff (two full builds). ## Evidence Back-test against first-push CI failures from a 90-day sample of merged PRs (26 rows; 2 retired checks; detached worktrees at each failing head): | Row | CI failure | Hook before | Hook after | |---|---|---|---| | openclaw#122960 | docs MD051 link fragments | pass | **fail**, same 3 errors as CI | | openclaw#112339 | Swift protocol models drift | pass | **fail**, same drift | | openclaw#115577 | native i18n inventory drift | pass | **fail**, same message | | openclaw#165543 | config baseline hash | pass | **fail**, same hash mismatch | | openclaw#151896 | line-cap growth 3557 → 3676 | pass | **fail**, same counts | The other 19 live rows need typecheck, tests, UI build or slow audits, which stay in hosted CI by design. Size check back-test: it first fails at 187e04d (openclaw#167739, 20,414 chars), and fails again at 057536d (openclaw#173903) after the earlier trim. Freshness step wall time, median of 3 on a warm checkout (before = previous head 7aae979, after = this head): | Staged change | Checks | Before | After | |---|---|---|---| | nothing | none | 0.17 s | 0.28 s | | `CHANGELOG.md` | none | 0.20 s | 0.24 s | | one docs page | lint:docs | 0.92 s | 1.13 s | | one `src/**/*.ts` | line-cap | 1.29 s | 1.42 s | | a scoped `AGENTS.md` | size check | 0.92 s | 1.07 s | | protocol schema `.ts` | protocol, line-cap | 4.77 s | 4.39 s | | Android Kotlin source | protocol, native i18n | 4.41 s | 4.48 s | | plugin manifest | config docs | 8.19 s | 8.97 s | Spawning `node` directly saves about 0.1–0.3 s per check against `pnpm -s`; the after column also pays one extra `git diff` for the unstaged-input rule. Most of the remaining time is in the checks themselves. `protocol:check` and `config:docs:check` are not cached: their inputs are transitive imports across `src/`, `packages/` and every plugin manifest, and no existing owner lists them reliably. Checks run: new and existing hook tests, including `test/git-hooks-pre-commit-freshness.test.ts` (real guard, real freshness step and a real `node` runner: a partly staged docs page with a blocked literal in its unstaged edit is skipped; a staged protocol schema with an unstaged edit in an imported owner outside the trigger list skips `protocol:check` while the line-cap ratchet still runs; and a failing check that prints a private literal is redacted), `test/scripts/check-staged-freshness.test.ts`, `changed-lanes.test.ts`, `check.test.ts`, `tsgo:scripts`, `tsgo:test:root`, oxlint, oxfmt. All commits went through the new hook. **Merge danger:** two-way door; a revert restores the previous hook and CI step. No state, config or published contract changes. Blast radius: every local commit in this repository (adds about 0.3 s to 9 s depending on staged paths) and the CI check-docs job. openclaw#176053 has landed, so the size check passes on `main`. Co-authored-by: Ayaan Zaidi <[email protected]>
Fixes #151792
What Problem This Solves
Feishu sends files with captions and multi-file messages as rich-text
postpayloads with a top-levelfiles[]array. The parser previously retained only the caption and inline media, silently dropping those files.User Impact
Captioned and files-only posts now deliver their files through the existing resource download and attachment-storage path. Top-level files retain standalone-document semantics; existing inline media remains video even when its filename has no recognized extension. Persisted replay identities remain unchanged, so redelivery after an upgrade does not run an old message again.
Why This Change Was Made
The existing post parser collects validated top-level file keys, ignores folders and invalid keys, and avoids duplicate downloads. The replay-key reader explicitly retains the shipped inline-only projection. No configuration, public SDK, database schema, retention, or permission changes are required.
The follow-up removes stray closing tokens from the existing handler test, preserves inline-video classification, and consolidates the two post-file handler cases into the existing handler fixture instead of keeping a separate duplicated runtime mock and test-routing entry.
Original implementation: @gokay-ai. Report and document-classification diagnosis: @WhitenessTiger. Related alternative: #153934.
Maintainer intent: Sarah's #158307 only adds an unrelated Agents API persistence-test entry to the shared test-routing file; that entry is unchanged. No overlap with Pash changes or Sarah's production behavior.
Evidence
An isolated, built Gateway received signed Feishu webhooks with the bundled Feishu plugin pointed at a local HTTPS API. The Lark token exchange, resource download, attachment storage, and agent-dispatch context were real. A proof plugin captured the public
reply_dispatchcontext instead of calling a model; no real Feishu account was used.Baseline: current main
00f3a2063022388cb7907964fe098537fbe3fd15.files[]files[]Both webhook responses were HTTP 200. The candidate attachment was read from the path in the actual agent context and compared byte-for-byte with the mock API response. Its SHA-256 was
39cc4c3d88a39a68781faf6c3c3d7f877fbf6d51875cc2d8a82fbc775beab0a7; MIME wastext/csv, kind wasdocument. Gateway proof took 13.59s on baseline and 13.10s on candidate, including startup and shutdown.The baseline media regression had three failures (captioned, multi-file, and unnamed top-level files all yielded no downloads) and two passing controls. The candidate parser/media/replay group passed all 36 tests. Existing handler and durable-ingress cases passed 115 tests, including the seeded pre-upgrade replay record. The consolidated captioned/files-only handler cases passed separately.
Limitations: this is isolated Gateway/channel-boundary proof using a local API and a substituted model backend, not a live Feishu service or model-provider test. Full repository checks were not run locally; hosted CI owns those lanes.
Final focused runs all passed with
--maxWorkers=1(wrapper wall times):post.test.tsbot-content.media.test.tsdedupe-key.test.tsbot.test.tsfeishu-ingress.test.tsThe two consolidated handler cases also failed against the current-main parser: the captioned case had no media paths and the files-only case never reached agent context. Restoring the candidate parser passed both. The existing handler fixture now covers these cases without the former 468-line duplicated runtime mock.
git diff --checkpassed.CI follow-up
Run 36811635616 passed the changed extension bundle (267s), compact-large shard (60s), and changed-boundary shard (129s), with zero cancelled jobs. Its test-type check caught an optional
saved.idin the newly consolidated mock. The follow-up uses the required resource key as that ID. The full extension-test typecheck and both affected handler cases then passed in secretless Docker. This is a test-only correction; the Gateway byte proof's production inputs are unchanged.Legacy replay follow-up
The exact-head review found one additional replay consumer: the monitor's retained raw-message-ID bridge. It now uses the same inline-only parser projection as the replay-key owner. The existing real-monitor replay fixture was extended with a timestamped captioned
files[]redelivery after a seeded raw-ID record and database reopen. It failed before the correction (two agent dispatches instead of one), then passed afterward; fresh-post and inline-image revision controls remain covered. Wrapper wall was 96.58s, including a cold 65.66s worker compilation; the test itself took 1.987s.The corrected production runtime was rebuilt and the signed-webhook Gateway byte proof was rerun successfully: the captioned post delivered the identical CSV document, and the attachment-free control still delivered only its caption. No Pash/Sarah history overlaps the newly touched monitor files.