* fix(line): run post-ack webhook processing on its own admitted work root
LINE acks the webhook and dispatches event processing fire-and-forget on
the same async chain. The HTTP request admission that chain inherited is
released as soon as the route handler returns, and a released admission
refuses subordinate queue work - so every LINE inbound agent turn fails
with "GatewayDrainingError: Gateway is draining; new tasks are not
accepted" even though the gateway is healthy. DMs, group mentions, and
postbacks are all affected; the user-visible symptom is the bot replying
"Sorry, I encountered an error processing your message." to everything.
Add runDetachedWebhookWork to the plugin-sdk webhook-request-guards
surface (a thin wrapper over the gateway independent-root continuation,
the same shape core uses in gateway/server/hooks.ts) and route all three
LINE ack-first dispatch sites through it: the gateway monitor handler
(the live path), and the createLineNodeWebhookHandler / Express
middleware handlers (public webhook building blocks an embedder can
register under the gateway). #65375 unified these three into one ack-first
pattern; keeping the detach consistent avoids re-introducing the same
latent defect in the two that are not on the live gateway path today.
The continuation is reserved synchronously while the request is still
admitted, so the detached processing stays accepted and a real restart
drain can wait for it instead of stranding it mid-turn.
Tests pin every layer: the guards suite proves detached post-ack work is
admitted after the request admission is released (and that the inherited
chain without the helper is refused); the monitor lifecycle suite and the
webhook-node suite assert each dispatch site goes through the detached
root. Red/green verified: reverting any dispatch fails its test.
* fix(plugin-sdk): account for runDetachedWebhookWork in public surface budget
* fix(channels): track detached webhook processing
Co-authored-by: 許元豪 <146086744+edenfunf@users.noreply.github.com>
* docs: refresh generated docs map
* chore(plugin-sdk): refresh API baseline
* fix(webhooks): preserve post-ack ordering
* test(plugin-sdk): satisfy detached work lint
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(line): wire base.allowlist config-edit adapter
LINE was the only DM/group channel without a base.allowlist adapter, so
`openclaw allow line` / the /allowlist command replied "does not support"
instead of editing the LINE allowlist. Reuse buildDmGroupAccountAllowlistAdapter
(DM + group + per-group-override scopes), matching Telegram/Signal; entry
normalization flows through the existing lineConfigAdapter.formatAllowFrom.
* fix(allowlist): preserve inherited account entries
* fix(allowlist): preserve all-scope store edits
Co-authored-by: Eden <146086744+edenfunf@users.noreply.github.com>
* test(allowlist): import config type
Co-authored-by: Eden <146086744+edenfunf@users.noreply.github.com>
* fix(allowlist): preserve empty effective overrides
Co-authored-by: Eden <146086744+edenfunf@users.noreply.github.com>
* fix(allowlist): reject group-only store edits
Co-authored-by: Eden <146086744+edenfunf@users.noreply.github.com>
* fix(allowlist): preserve cleared channel overrides
Co-authored-by: Eden <146086744+edenfunf@users.noreply.github.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(line): avoid exposing media URLs in errors
* test(line): cover media URL redaction paths
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(line): retry inbound media download while LINE reports 202
LINE answers the content endpoint with 202 and an empty body for a
short window while it prepares inbound media. The previous single-shot
download saved that empty body as a 0-byte file, silently dropping the
user's media. Poll the content endpoint with capped exponential
backoff until it stops returning 202, releasing each empty response
before retrying, and throw a visible error if it never becomes ready.
* fix(line): clean up media retry responses
* fix(line): use plugin runtime fetch seam
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(line): keep group history recorded during a mention turn
Group history cleanup ran a whole-key clear after each mention turn. Because
webhook dispatch is fire-and-forget, a plain (unmentioned) message can be
recorded while the agent is still handling a mention; the post-turn clear then
wiped the whole window, silently dropping that concurrent message from the
next turn's context.
Snapshot the identity keys the turn consumes up front and clear only those,
retaining anything recorded concurrently. The record path now stamps the LINE
message id so entries have a stable identity. Cleanup stays after the turn, so
a failed turn still leaves history intact for the retry. The snapshot/clear
helpers live in group-history.ts, next to the other group modules.
* test(line): cover group history retention when a mention turn fails
Assert that a mention turn whose processMessage throws leaves the group
history window intact (cleanup runs only after a successful turn), so the
retry still has the ambient context. Red/green verified: moving the cleanup
before the await fails this test.
* fix(line): capture consumed group history at the context read boundary
The consumed-key snapshot ran before media download and context
construction, while buildLineMessageContext copied the window later.
A plain message recorded between those awaits was included in the
turn's InboundHistory yet missing from the consumed set, so cleanup
retained it and the next mention received it twice.
Read the window and capture its identity keys in one synchronous step
in the handler (snapshotLineGroupHistory), then pass the materialized
inboundHistory into buildLineMessageContext instead of the live map.
The consumed set is now derived from the exact entries the turn reads,
so an entry is either in the context and cleared, or recorded later
and retained - there is no window where both can be true.
Regression tests park a mention turn inside context construction,
record an ambient message mid-window, and assert it stays out of that
turn's InboundHistory, survives cleanup, and is consumed exactly once
by the next mention. Red/green verified against the previous snapshot
placement.
* test(line): point history-window guardrail at group-history.ts
The group-history cleanup fix moved the createChannelHistoryWindow facade
call out of bot-message-context.ts (which now receives inboundHistory as a
parameter) into the new group-history.ts. Update the cross-channel
historyWindowFiles ledger to match, mirroring telegram's group-history-window.ts
entry already in the same list. Fixes the message-turn-guardrails 'keeps
migrated history users on the channel history window facade' assertion that
build-artifacts flagged.
* fix(line): reserve history across concurrent turns
Co-authored-by: Eden <146086744+edenfunf@users.noreply.github.com>
* test(line): complete webhook message fixtures
* test(line): use explicit token placeholders
* refactor(line): keep reservation type private
* refactor(line): render reservations through history facade
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(line): honor channelData.line.mediaKind on the reply-token path
The reply-token delivery built every media message with createImageMessage,
ignoring channelData.line.mediaKind (and previewImageUrl/durationMs/trackingId),
so a video/audio reply was silently downgraded to a broken image. The push path
already honored mediaKind via resolveLineOutboundMedia + buildLineMediaMessageObject.
Route reply-token media through those same helpers (relocated to outbound-media.ts
and reused by both paths) via an injected buildMediaMessage dep wired in monitor.ts,
preserving the delivery file's dependency-injection boundary. Generic media without
LINE-specific options keeps the image route; a media that cannot be built surfaces
as a visible partial delivery so the text still reaches the user.
* refactor(line): unify reply media delivery
* fix(line): normalize media delivery failures
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(line): use precise control-command check for group mention bypass
Group requireMention bypass is gated on `command.hasControlCommand`, but LINE
fed it the broad `shouldComputeCommandAuthorized` detector, which is true for
any inline "/x"/"!x" token. An allowlisted member's plain message like
"cd /home" would satisfy the bypass and reach the agent even though the bot
was never mentioned.
Groups now use the precise `hasControlCommand` (message starts with a real
command); DMs keep the broad detector since they have no mention gate. This
mirrors googlechat's monitor-access wiring. The group/DM split lives in a
small `resolveLineControlCommand` helper in group-policy.ts, next to
resolveLineGroupRequireMention, so bot-handlers.ts stays within its size cap.
* test(line): assert group mention-bypass with a registered control command
The positive regression case sent `!status`, which the mocked
command-auth module treated as a control command via a naive
`startsWith("!")`. Production `hasControlCommand` matches the message
body against registered command aliases (e.g. `/status`), so `!status`
is only an inline token, not a real control command. The test could
therefore pass without proving that a genuine control command still
bypasses requireMention.
Use the registered `/status` alias for the bypass cases and make the
mock reflect the real split: `hasControlCommand` = starts with a
registered alias (precise); `shouldComputeCommandAuthorized` =
`hasInlineCommandTokens` regex over `/x`/`!x` (broad). The two
detectors are now distinguishable, so the group precise-detector path
is actually exercised.
* refactor(line): centralize command detection
* test(line): complete direct message fixture
* docs(changelog): credit LINE command fix
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(line): drop blank entries from trailing commas in agenda/device directives
* fix(line): drop blank device action labels
Co-authored-by: Eden <146086744+edenfunf@users.noreply.github.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(config): add canonical group-policy scope-tree resolver
* chore(plugin-sdk): refresh API baseline hash for scope-tree exports
* refactor(channels): migrate googlechat, imessage, and whatsapp group policy onto the scope tree
* chore(plugin-sdk): refresh API baseline hash for groups scope-tree builder
* refactor(channels): migrate line, qqbot, and mattermost group policy onto the scope tree
* chore(plugin-sdk): refresh API baseline hash for case-insensitive scope key helper
* chore(plugin-sdk): refresh API baseline hash after rebase onto current main
* chore(plugin-sdk): refresh API baseline hash after rebase
* chore(plugin-sdk): refresh API baseline hash after rebase
* chore(plugin-sdk): refresh API baseline hash after rebase
Prevent LINE group allowlist policy from inheriting DM allowFrom entries, so open DMs do not broaden group access. Keep group-specific allowlists and per-group overrides as the only group admission sources, and document the scoped behavior.
Co-authored-by: pgondhi987 <pgondhi987@users.noreply.github.com>
* fix(extensions): make indexed access explicit across channel plugins
Transport-payload-safe burn-down: malformed Telegram/Discord/QQ/LINE
and sibling channel input keeps existing skip paths; no synthesized
fields, no new throws in delivery loops. Zalo escape sentinels preserve
literal matches instead of undefined replacements.
* fix(extensions): make indexed access explicit across provider and memory plugins
Stream and model iteration, tool-block guards, capture guards, and
sparse accumulators; singleton model reads carry named invariants.
* fix(extensions): make indexed access explicit across tooling plugins, flip the extensions lane
Remaining plugins (oc-path, qa-lab, browser, logbook, and siblings) plus
the tsconfig.extensions.json flag flip. Cleanup: logbook sampleFrames
NaN index at max=1, QA retry clamp at non-positive attempts, dead Canvas
probe and OpenShell no-op slice removed, twitch test setup leak excluded
from the prod lane.
* refactor(plugin-sdk): expose expectDefined via a focused SDK subpath
Extensions imported @openclaw/normalization-core directly, crossing the
external-plugin packaging boundary (it only worked because the runtime
builder bundles undeclared workspace helpers). expect-runtime joins the
canonical entrypoints JSON, generated exports, API baseline, docs, and
subpath contract test; all 78 extension imports now use the SDK seam.
Two scanner-shaped locals renamed for review-bundle hygiene.
* chore(plugin-sdk): raise surface budgets for the expect-runtime subpath
One new entrypoint with one callable export, added intentionally as the
packaging-honest seam for extension invariant helpers.
* fix(matrix): truncate inbound preview on UTF-16 code-point boundary
The matrix inbound verbose preview used bodyText.slice(0, 200) before logging.
When a supplementary-plane character (emoji, extended CJK) straddles the 200th
code unit, the slice splits its surrogate pair and emits a lone surrogate into
the verbose log line. Lone surrogates corrupt JSON log serialization, break
terminal rendering, and crash UTF-8-validating log shippers.
Use the shared truncateUtf16Safe helper (already used by the mattermost sibling
monitor after #101630) to keep complete surrogate pairs intact. No behavior
change for ASCII input; the preview is still capped at 200 code units.
Co-Authored-By: Claude <noreply@anthropic.com>
* test(matrix): fix inbound-preview test for lint and lib target
- Avoid String.prototype.isWellFormed (ES2024 lib): use the encodeURIComponent
well-formedness probe instead, which works on the project's TS lib target.
- Replace literal string concatenation with template strings to satisfy the
no-useless-concat lint rule. No behavior change in the assertions.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(channels): keep inbound log previews UTF-16 safe
* test(line): type verbose preview fixtures
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(line): truncate outbound altText, location, menu, and code fields on code-point boundaries
* fix(line): use safe truncation for receipt card altText
* fix(line): count rich menu limits by grapheme
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
The multi-account resolver had two bugs that prevented webhook routes
from registering:
1. `accounts.default` was ignored because `resolveLineAccount` short-
circuited the account lookup whenever `accountId` resolved to
`DEFAULT_ACCOUNT_ID`. Credentials placed under
`channels.line.accounts.default` therefore never reached the
gateway, and the default `/line/webhook` route never registered.
2. Named accounts defaulted to disabled when they did not explicitly
set `enabled: true`. A configured second account
(`channels.line.accounts.<name>`) was treated as disabled before
`startAccount` ran, so its webhook never registered either.
Both behaviours diverged from the Telegram plugin, which uses
`baseEnabled && accountEnabled` with each defaulting to
`enabled !== false`. Align the LINE resolver with that idiom and add
seven regression tests covering the credential lookup, the default-
enabled semantics, and channel-level disable propagation.
* fix(line): truncate template title/altText on grapheme boundaries, not raw UTF-16
createConfirmTemplate/createButtonTemplate/createTemplateCarousel/createCarouselColumn/
createImageCarousel truncated title and altText with a raw `.slice(0, N)`, so an
emoji straddling a LINE field limit (e.g. a 40-char button title) was cut in half,
leaving a lone high surrogate that LINE renders as the replacement char or rejects.
Route those fields through the file's existing grapheme-safe truncateTemplateText
(already used for the text body) via a small truncateOptionalTemplateText wrapper.
Byte-identical for all-BMP input; only straddling-emoji truncation changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: retry OpenGrep scan (HTTP 502 infra flake)
* test(line): cover grapheme-safe template fields
---------
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Use surrogate-safe truncation for LINE action labels/data, including card-command, markdown link, quick-reply, and media-control action surfaces, with focused regression coverage.
* chore(release): close out 2026.6.10 on main
* chore(release): align native app metadata for 2026.6.10
* chore(release): sync Android 2026.6.10 notes
* docs(changelog): preserve 2026.6.9 history
* docs(changelog): preserve 2026.6.9 history