* feat(channels): add channel-owned setup contracts
* test(channels): align legacy setup fixtures
* chore(channels): regenerate config and SDK baselines after rebase
* fix(update): run fresh doctor after current-process core changes
* fix(channels): align add pre-scan with execution precedence
* style(cli): format channels-cli test additions
* fix(channels): restore option-before-positional channel resolution via metadata arity scan
* fix(channels): keep help flags out of metadata arity escalation
* test(update): mock fresh post-update doctor in current-process suites
* style: format review fixes and correct entrypoint mock type
* fix(channels): register only modern contract options for dual-publishing plugins
* test(update): align downgrade suites with fresh-doctor child invocation
* docs(channels): record empty-contract and input-forwarding invariants
* fix(line): keep the shipped --token switch as a channel access token alias
* fix(signal): stop treating exact cross-family loopback endpoints as bind-aligned
* chore(config): regenerate docs config baselines after second rebase
* style: format rebased channels add tests
* fix(channels): enforce field-key and flag-name agreement in setup contracts
* fix(signal): detect container endpoints for bare --http-url setup
* fix(signal): ignore unconfigured accounts in transport collision checks
* fix(channels): validate negated setup flags in contract and normalizer
* fix(signal): preserve existing transport kind when setup detection is unreachable
* style(signal): use direct boolean check in collision guard
* style(signal): type test config literals
* docs(update): record two-read design of fresh-doctor validation gate
* fix(channels): satisfy post-rebase architecture gates
* docs: refresh channel setup map
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(channels): restore transcript context after restart
Merge bounded active-branch session transcript turns at the shared prepared-turn seam so message channels retain assistant replies after restart or history eviction. Migrate Telegram's one-off merge while preserving exact projection and legacy dedupe behavior.\n\nCloses #112520. Slack case reported by Joe Tam (@joetam) in #102594.
* style(channels): avoid spread in transcript mapping
* refactor(telegram): drop obsolete transcript exports
* refactor(media): ingress and preflight consumers ride ordered media facts
Media-facts program PR 2: channel context retains ordered MediaFact[]
through finalization; inbound gates, hook mapping, current-turn
normalization, and all five channel families' audio preflights consume
facts. Four review cycles hardened the canonical/legacy merge: canonical
facts are authoritative (legacy fills gaps only), one shared staging
predicate covers MediaStaged and MediaWorkspaceDir, kind inference runs
after fallback MIME fills, legacy counts use max array cardinality, and
alignment-only empty slots no longer count as media presence — all
pinned by a 54-cell canonical×legacy×type merge-matrix test.
* fix(media): hide internal fact helper
* fix(feishu): normalize media upload multipart data
Convert Feishu SDK multipart Buffer upload parts into explicit FormData
before they reach the wrapped HTTP transport. The SDK upload helpers
pass multipart data as a plain object with Buffer media parts; relying on
implicit serialization in the timeout/proxy-aware HTTP wrapper was fragile
and caused file/image delivery failures (400 volc-dcdn / write ECONNRESET)
even though the same credentials succeeded via a standalone SDK upload.
Adds normalizeMultipartUploadData, applied on the request path of the
shared Feishu HTTP instance, plus a regression test covering the
multipart-to-FormData normalization.
Rebased onto current origin/main, which replaced the old synchronous
injectTimeout path with the async proxy-aware injectRequestOptions flow;
the multipart normalization is now applied before that flow.
* fix(feishu): scope multipart media normalization
* test(feishu): mark multipart auth fixture synthetic
* test(feishu): use field-shaped secret fixtures
* fix(feishu): validate multipart upload endpoints
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(feishu): fall back media replies
* test(feishu): polish media fallback coverage
* test(feishu): cover media fallback policy wiring
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* refactor(media): consolidate parallel media-kind unions onto canonical MediaKind
One canonical MediaKind union (media-core constants) replaces ~40
duplicate/parallel kind declarations across core and channel plugins;
channel-specific narrower contracts derive via Extract/Exclude. Also
fixes a review-caught fallback bug where a stored "unknown" reply-chain
kind preempted MIME inference and relabeled images as documents.
* refactor(ui): derive attachment kinds from MediaKind
* fix(telegram): drop type-dead unknown guard in reply-context kind fallback
* style(telegram): format media kind fallback
* refactor(channels): shared supplemental sender gating, allowlist-match adoption, outbound mop-ups
* fix(plugin-sdk): skip-aware media sequence with text fallback for empty URLs
* chore(plugin-sdk): align surface budgets after rebase
* test(qqbot): type media sender mock calls
* fix(plugin-sdk): distinguish empty media sequences
* fix(plugin-sdk): track void media sends
* Fix Feishu local media fallback leaks
* refactor(feishu): clarify media fallback link style
* fix(feishu): reject credentialed media fallbacks
* fix(feishu): reject ambiguous media fallback URLs
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(feishu): honor abortSignal in drive comment reply retry loop
The inter-attempt delay in monitor.comment.ts used a bare setTimeout and
never received the owning monitor account's AbortSignal, so an account
teardown (ws disconnect, webhook stop, config reload) left the in-flight
comment reply retry running for up to ~6s after the rest of the lifecycle
settled. delayMs now wraps waitForAbortableDelay from async.ts and the
abortSignal is threaded through fetchDriveCommentContext →
resolveDriveCommentEventTurn → handleFeishuCommentEvent →
createFeishuDriveCommentNoticeHandler, sourced from
monitorSingleAccount.
The retry loop checks the waitMs return value and the abortSignal flag,
breaking out of the next 1s tick instead of running the full 6×1s poll
budget. Sibling surface monitor.bot-identity.ts already uses the same
pattern; #104431 explicitly skipped the comment-monitor retry because
it is a result-predicate poll, not an error retry, and this fix is
orthogonal to that decision.
Not tested: live Feishu tenant, real bot identity, real drive comment,
and account restart. requestFeishuOpenApi itself does not accept
AbortSignal — that is a separate, larger refactor and is out of scope.
* test(feishu): prove comment retry abort
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(presentation): support hosted widget actions
* refactor(discord): route widget launches through presentation
* chore: keep changelog release-owned
* test(interactive): type the invalid web-app boundary case explicitly
* test: type web-app action fixtures against the split union
The two-member web-app union no longer accepts a distributed
type: "url" | "web-app" literal or an empty receipt in typed
callbacks; use explicit action casts and the Discord receipt factory.
* fix(feishu): add chunk-idle timeout to inbound media download
* fix(feishu): extract media chunk-idle helper under LOC ratchet
* fix(feishu): keep only used media idle export for knip/lint
* test(feishu): add executable negative control for stalled media hang
* test(feishu): avoid dangling underscore in negative control
* test(feishu): add production-path idle proof with real saveMediaStream
* fix(feishu): drop await in media idle finally so timeout escapes stalled Readable
The finally block's `await iterator.return()` blocked the timeout error
from propagating when the source is a real Node.js Readable.
Readable async-iterator `return()` does not settle on stalled streams,
so awaiting it made the whole idle timeout a no-op for that boundary.
Add Lark SDK boundary tests with real Readable streams (stalled,
progressing, multi-chunk) and production-path proof.
* fix(feishu): thread canonical Feishu timeout through media download
Derive the per-chunk idle timeout from resolveConfiguredHttpTimeoutMs()
so the established Feishu HTTP-timeout policy (env var, account config,
default) is preserved instead of a hardcoded 30s fallback.
Co-Authored-By: nebulacoder-v8.0 <noreply@zte.com.cn>
* fix(feishu): restore sanitizeFileNameForUpload export lost in cherry-pick
The extraction commit (extract media chunk-idle helper) removed the
export from media.ts and the let declaration from media.test.ts during
the cherry-pick because origin/main does not export this function.
Co-Authored-By: nebulacoder-v8.0 <noreply@zte.com.cn>
* fix(feishu): destroy stalled source on chunk-idle timeout
Destroy the underlying Readable source when the idle timer fires so the
Lark SDK stream and its HTTP resource close instead of leaking. Previously
only iterator.return() was called without awaiting it, which dropped the
cleanup promise on stalled Node Readables without releasing resources.
Add assertions in tests and proof that the source Readable is destroyed
after timeout.
Co-Authored-By: nebulacoder-v8.0 <noreply@zte.com.cn>
* fix(feishu): drop unused export and compact timeout calls to stay under LOC ratchet
Removes the `export` from sanitizeFileNameForUpload (never imported outside
media.ts and its test) and uses a local `save` wrapper in
saveFeishuResponseMedia to keep the chunk-idle timeout calls single-line so
media.ts stays under its baseline. The test no longer imports the
now-private helper.
Co-Authored-By: nebulacoder-v8.0 <noreply@zte.com.cn>
* chore: trigger CI after rebase onto main
* chore: trigger CI after rebase onto main
* test(feishu): add HTTP loopback proof for media chunk-idle destroy
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(feishu): clear media idle proof oxlint
* fix(feishu): bound stalled inbound media streams
Co-authored-by: Pick-cat <huang.ting3@xydigit.com>
* test(feishu): avoid promise executor return
Co-authored-by: Pick-cat <huang.ting3@xydigit.com>
* fix(feishu): keep media timeout authoritative
Co-authored-by: Pick-cat <huang.ting3@xydigit.com>
---------
Co-authored-by: nebulacoder-v8.0 <noreply@zte.com.cn>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* refactor(plugin-sdk): narrow wildcard barrels to explicit used exports
* refactor(tools): delete dead tool-planning module exposed by barrel narrowing
* fix(plugin-sdk): restore deprecation tag on OpenClawSchemaType alias
* test(agents): drop test for deleted runtime proxy module
* refactor(tools): trim descriptor types to cache consumers
* refactor(deadcode): harvest exports orphaned by barrel narrowing
* refactor(deadcode): harvest exports orphaned by barrel narrowing (rest)
* fix(agents): restore sdk imports and test markers via public predicate
* fix(plugin-sdk): named type re-exports in plugin-entry; trim types barrel precisely
* chore(plugin-sdk): account unmasked deprecated provider types in budgets
* fix(plugins): name star-only type rows for dts bundling
* fix(plugins): restore host-hook surface; unexport internal api compositions
* fix(plugins): named type imports for api composition; restore needed source exports
* fix(plugins): knip-visible type imports for registry surfaces
* test: adapt tests to privatized media and command internals
* fix(qa-lab): re-export snapshot conversation type
* style: format sessions sdk imports
* fix(plugins): restore smoke entry export; pin budgets to exact actuals
* fix(plugins): canonical smoke-entry import; drop orphaned root shims
* fix(plugins): allowlist manifest probe, repoint qa web import, drop dead browser barrels
* fix(plugin-sdk): pin codex auth marker and scaffold provider type
* fix(qa-lab): keep web-facing model-selection shim within boundary rules
* fix(plugin-sdk): preserve merged contracts through narrowed barrels
* chore(plugin-sdk): pin post-rebase surface budgets
* chore(plugins): gate deprecation hygiene in CI and purge internal deprecated usage
* fix(agents): migrate auth-profiles oauth import to OAuthProviderId
* fix(whatsapp): use durability param in delivery-recovery test
* chore(scripts): declare shouldRunDeprecationHygieneChecks in check-changed types
* test(plugins): update runtime-api guard for ssrf policy rename
* refactor(plugin-sdk)!: delete the zero-consumer channel-ingress facade, AccessFacts projections, and dead dispatch aliases
* chore(plugin-sdk): refresh API baseline hash for the facade deletions
* fix(channels): drop dead AccessFacts field writes stranded by the facade deletion
* chore(plugin-sdk): refresh API baseline hash after rebase
* fix(feishu): upgrade single newlines to paragraph breaks in post md messages
* fix(feishu): move post-md normalization out of adapter chunker, add card-mode regression test
* fix(feishu): normalize post-md newlines before chunk/limit decisions
* fix(feishu): remove post-md normalization from outbound chunker
Move newline normalization out of feishuOutbound.chunker so card and
document-comment text paths are not affected. Post-md normalization
remains scoped to sendOutboundText, buildFeishuPostMessagePayload, and
editMessageFeishu where the render mode is known.
Add regression test verifying the chunker preserves single newlines
unchanged.
Ref: https://github.com/openclaw/openclaw/pull/99394#issuecomment-4873230477
* fix(feishu): route card on raw text, add post-md expansion length guards
* fix(feishu): scope reply target to first subchunk in expanded post fanout
* fix(feishu): skip redundant post-md normalization for pre-chunked outbound and reply text
ClawSweeper review identified that subchunks from already-normalized
post-md text re-enter sendMessageFeishu which normalizes again. If a
chunk boundary falls inside a fenced code block the second chunk lacks
the opening fence, so findCodeRegions treats code newlines as prose and
expands them to paragraph breaks.
Add alreadyNormalized flag to buildFeishuPostMessagePayload and
sendMessageFeishu so pre-normalized callers (sendOutboundText overflow
path and reply-dispatcher sendChunkedTextReply) can skip the redundant
normalization pass.
* fix(feishu): convert markdown tables before post-md newline normalization in outbound path
ClawSweeper identified that sendOutboundText normalizes newlines before
sendMessageFeishu runs convertMarkdownTables. The blank lines inserted by
normalization break markdown table row contiguity, so raw-mode Feishu
accounts can lose table rendering while fixing paragraph spacing.
Convert tables before the newline upgrader so the converter sees the
original table structure. sendMessageFeishu runs convertMarkdownTables
again internally, but that is a no-op on already-converted text.
This matches the reply-dispatcher path which already converts tables
before normalization.
* chore(feishu): avoid exporting internal card builders, test through public send API
The card-mode newline preservation tests now exercise sendMarkdownCardFeishu
and sendStructuredCardFeishu instead of calling buildMarkdownCard and
buildStructuredCard directly. This keeps the SDK surface unchanged from
upstream/main.
The schema-2.0 width config and buildStructuredCard header-fallback tests
are removed because upstream/main already covers them under the sends
cards with schema-2.0 width config describe.
* fix(feishu): preserve markdown newlines in posts
* fix(feishu): preserve multiline markdown structure
* fix(feishu): preserve markdown structure while chunking
* fix(feishu): satisfy package and type gates
* fix(feishu): chunk posts within byte envelope
* fix(feishu): restore channel chunker contract
* chore(feishu): leave release notes release-owned
* chore(feishu): shrink max-lines baseline
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>