* feat(reef): operator-configurable sharing rules for the guard
Adds channels.reef.guard.rules with capped outbound/inbound free-text
policy. Rules ride the trusted instruction side of the guard call only,
may tighten decisions or explicitly allow otherwise-review cases, and can
never override the deny floor or deterministic checks. The rules text is
hashed into the effective policy version so audit rows and pending review
approvals bind to the exact policy in force.
* fix(reef): full rules digest in policy identity; align blank-rule validation
ClawSweeper review fixes: use the untruncated sha256 rules digest in the
effective policy version so approvalDigest stays collision-resistant, and
express identical non-blank (\S) rule validation in the zod schema, the
manifest JSON Schemas, and the generated channel metadata instead of a
trim-transform mismatch.
* fix(twitch): retain native sender identity in execution audits
Forward the registered channel runtime and actual account identity, keeping native participants distinct from role authorization. Normalize the all role at the Twitch policy producer and preserve unknown and disabled-collection paths.
Cover actual registered-plugin ingress and native reply delivery, and drain asynchronous monitor test work before cleanup. Related to #122863; no shared authorization or storage change.
* test(twitch): track the registered context builder in inventory
* test(twitch): honor host and plugin contract boundaries
Introduce on admitted bot joins using the existing sealed no-tools turn and durable room dedupe. Matrix uses room metadata and readable history; LINE uses group names and explicitly unavailable history.
Mark Matrix bridge membership provenance without changing existing event delivery, invite handling, or auto-join. Exclude startup snapshots and already-joined profile updates, including startup connection recovery.
Simplify snapshot budget accounting while preserving prompt bytes, metadata order, and oldest-first message dropping. Add channel config, docs, and regression coverage.
* fix(slack): record reasons for failed and omitted attachments
Keep failed and capped files visible in agent context with bounded, redacted reasons and unified unavailable attachment accounting. Log final failures once while preserving download limits, URL refresh retries, authentication, and HTML rejection.
* test(slack): align forwarded-image assertion with unified unavailable notice
* fix(agents): preserve replies after unavailable approvals
Keep setup notices durable without suppressing recovered answers or terminal errors. Preserve genuine pending-approval deduplication and align native approval setup guidance. Fixes#130584.
* fix(discord): restore named-account research skill discovery
Gate the Discord skill on channel configuration instead of a root token. Make clawtributor research portable across archive and native history readers, and apply requested time windows to conversation references rather than PR creation dates.
* fix(agents): order tool-result delivery before assistant events
* fix(slack): correct native approval enablement guidance
* test(qa): support context-aware model fixtures
* test(scripts): publish ready process IDs atomically
Keep raw recovery keys in explicitly opted-in JSON output. Add a safe text
hint in the CLI-owned result renderer, remove duplicated stored-key output,
and cover normal, verbose, missing-key and JSON command behavior.
Thanks @hartmark for the original fix and real-account reproduction.
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(channels): introduce bots when they join group rooms
* feat(channels): add Discord and Telegram join introductions
* fix(channels): isolate untrusted evidence and select allowed join targets
* refactor(channels): scope joinIntro to implementing channels
* fix(channels): keep a delivered join introduction settled when its durable commit fails
* feat(channels): read more room history and document join introductions in detail
* chore(config): regenerate bundled channel metadata after rebase
Make ask_user use native Telegram controls when one single-select question can be represented directly, while keeping multi-select and batch questions on the text fallback. Add native Other-to-ForceReply input, preserve Gateway-owned option ordering across Telegram, Slack, and Discord, and prevent later progress from overwriting the delivered question.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* feat(buzz): retain bounded passive context for accepted turns
Add opt-in connection-owned room and thread context using the shared history window. Preserve sender admission, current roster filtering, UTF-8 bounds, and snapshot consumption without passive inference, session recording, or typing. Related: #129599.
* docs(config): align baseline with landed Feishu sticker sets
Use the shared per-Gateway bot-pair budget with the latest received signed room roles. Preserve existing sender and mention admission and human traffic. Release note: bound repeated Buzz bot exchanges without adding a channel-specific policy or persistent state.
* feat(feishu): search configured received stickers by keyword
Bind curated sticker sets to the selected bot app ID and reuse the existing sticker action gate and native sender. Enforce matching scalar bounds in runtime and generated config schemas, with bounded search output and explicit truncation. Completes the configurable-set request in #114578.
* refactor(feishu): keep sticker schemas with config owner
* build(feishu): regenerate catalog metadata after channel schema update
* fix(channels): bundled channels reject the documented responsePrefix override
* fix(feishu): pass channel and account to the reply prefix resolver
The ordinary reply dispatcher built the prefix context without channel or
accountId, so channels.feishu.responsePrefix and its account override fell
through to the global value. The comment dispatcher already passed them.
* fix(channels): accept twitch's root responsePrefix and type ClickClack's
Twitch validates as a two-branch union with both branches closed, so
responsePrefix declared only on the account shape left
channels.twitch.responsePrefix rejected whenever accounts is present. Also
declares responsePrefix on ClickClackAccountConfig so typed callers can express
what its schema already accepts.
* fix(clickclack): apply the configured responsePrefix in model mode
Model mode sends the completion straight to ClickClack instead of going through
the agent reply pipeline, so the documented responsePrefix parsed and then did
nothing. Resolve it through the public reply-pipeline seam so template
variables like {model} render from the completion's own provider and model.
* fix(clickclack): do not double the response prefix in model mode
systemPrompt is operator-owned, so a model can be instructed to emit the
configured prefix itself and the unconditional concatenation then sent it
twice. Match the shared reply normalizer's startsWith guard.
* test(clickclack): resolve inbound access through the real resolver
Main now requires access.channelIngress, which the hand-built fixture did not
provide, so the model-mode messages never dispatched. Drop the fixture and let
handleClickClackInbound resolve access from a DM-shaped message, matching the
sibling bot-loop tests.
* test(channels): reject a key missing from any composed branch
A union alternative is a configuration mode an operator can pick, so a key
present in only one mode is unusable in the other even though the union still
validates. Treat any rejecting alternative as a rejection, and walk allOf when
collecting account schemas.
* fix(channels): offer responsePrefix only where the reply path applies it
Applying the prefix to an outbound reply is per-channel wiring, not a shared
step. Buzz, Nostr, QA Channel, Raft, Reef and SMS never read it, so accepting
the key there validated a setting their delivery path ignores. Anchor the
contract to the channels that consume the prefix.
* fix(channels): complete shared response prefix delivery
Co-authored-by: ayaangazali <ayaangazali.work@gmail.com>
* docs(config): refresh response prefix baseline
* test(tlon): use canonical prefix fixture helpers
* test(channels): prove Twitch reply prefix delivery
Exercise schema admission and the real shared reply dispatcher at the Twitch native send boundary, replacing redundant schema-only coverage. Keep Tlon fixtures on canonical imports and recording options.
* docs(channels): clarify explicit message prefix behavior
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(feishu): resend received stickers without losing topic routing
Add opt-in agent sticker sending using received file keys, preserve inbound keys, and stop unsupported sticker resource downloads. Reuse keyed-media delivery and canonical reply selection; inherit source topics only within the same destination and account. Related: #114578.
* fix(feishu): preserve topic mode for prepared implicit replies
* docs(feishu): preserve account action gates when enabling stickers
* feat: make model persistence scope explicit
* fix: keep unauthorized model directives as plain text
An unauthorized sender's inline directives are cleared to plain text before
they are acted on, but the persistent write target was derived from the
directives as they arrived. An unauthorized `/model <provider/model> -a` or
`-g` therefore reached the owner-authority error return instead of the
plain-text path every other directive takes.
Derive the target, the authority flag, and the persist flag after the
clearing. Authorized senders skip the clearing, so their behavior is
unchanged; the only closure that captures the target is invoked well after
the new declaration site.
Covers both persistent flags with a regression test that fails on the prior
head with "Agent and global model defaults require owner authority or
operator.admin scope." and passes here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(models): make model selection scope configurable
Preserve current model-selection defaults when the optional scope preference is unset. Add explicit session, agent, and global overrides without broadening config-write authority.
Co-authored-by: Marvinthebored <peter@lindsey.jp>
* test(models): simplify scope expectation fixtures
---------
Co-authored-by: Marvinthebored <peter@lindsey.jp>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(discord): prevent voice replies from cutting off during playback
Preserve realtime PCM through normal backpressure and release queued speech only after the Discord player finishes. Tolerate brief provider stalls, remove the STT/TTS completion deadline, and settle playback immediately on terminal session shutdown.
* test(discord): remove unused playback test bindings
* feat(a2a): add A2A v1.0 agent-interop channel plugin
Adds `a2a` as a bundled, default-off channel plugin so A2A-compliant agents
(Hermes Agent, LangChain, CrewAI, Google ADK) can discover an OpenClaw gateway
and send it tasks, and so OpenClaw can address configured peers.
Inbound HTTP (`/.well-known/agent-card.json`, `/a2a/v1`) runs through the normal
channel ingress path, so peer messages inherit allowlist admission, provenance
framing, and session routing. The agent reply returns through the channel
delivery callback and resolves the A2A task with its artifact, correlated per
(peer, contextId) FIFO so concurrent sends cannot cross-talk.
Wire format follows the canonical v1.0.0 spec sources (a2aproject/A2A
specification/a2a.proto): PascalCase SendMessage/GetTask/CancelTask,
supportedInterfaces[] rather than the 0.3-era top-level url/protocolVersion,
member-presence Part oneof, TASK_STATE_*/ROLE_* enums, no Task.kind. The 0.3
dotted method names are accepted as explicit compat aliases because shipped
Hermes-generation peers still send them.
Security: per-peer bearer tokens compared in constant time, per-peer sliding
window rate limit, 1 MiB body cap, 64 KiB inbound text cap, tasks scoped to
their owning peer, outbound redirect: "error", and no inbound-controlled target
URLs. With no peers configured the channel stays unconfigured and registers no
routes.
Live-verified against a real gateway on an isolated state dir with a mock
provider: 31/31 proofs, including the agent reply round-tripping into the task
artifact. That run caught two defects fixed here:
- The agent card read `cfg.agents.list` directly, so gateways configured with
the canonical `agents.entries` roster advertised zero skills. Now uses
listAgentIds/resolveAgentConfig, which read either roster shape.
- `returnImmediately` dispatches inherited the HTTP request's released work-
admission root and every async task failed as GatewayDrainingError. Now
reserves an independent root via runDetachedWebhookWork, matching sms/line/zalo.
* fix(a2a): route outbound sends through the SSRF guard and land setup metadata
CI on the first push surfaced four real defects that local `check:changed` did
not cover (it runs no oxlint or typecheck lane):
- Outbound peer sends used raw `fetch()`, tripping the channel/plugin
no-raw-fetch boundary. They now go through `fetchWithSsrFGuard` with
`maxRedirects: 0`, so A2A egress carries the same SSRF policy as every other
plugin call and a redirect cannot re-target a delivered task.
- The channel declared an empty setup contract while its package manifest
declared none, so the bundled-channel shape contract failed. The wizard now
collects `advertisedUrl` plus a first peer name/token pair, which is the
minimum that leaves A2A actually configured (it stays unconfigured until one
peer credential exists), and the package manifest mirrors that metadata.
- Six lint errors in the test files: base-to-string on `BodyInit` bodies, an
unused mock, and a shadowed `options` parameter. Request bodies now go through
a typed reader that asserts the serialized-string contract before parsing.
- A card assertion referenced `capabilities` without declaring it on the local
response type, failing test typecheck.
Outbound redirect handling moves from `redirect: "error"` to the guard's
`manual` inspection; the adapter tests assert the new shape.
* fix(a2a): drop route replaceExisting, accept configWrites, register runtime api
CI after the interop run surfaced four more gates:
- `registerPluginHttpRoute` used `replaceExisting: true`, tripping the
GHSA-RQP8-Q22P-5J9Q rule: a duplicate path can silently replace another
account's handler. A2A owns fixed global paths on a single account, so a
duplicate registration means a stale or conflicting owner. It now fails loudly
through `throwOnFailure` instead.
- The channel schema rejected `channels.a2a.configWrites`, which every bundled
channel must accept; added to the zod schema, the manifest schema, and the
config type, then regenerated the channel metadata.
- The new `input` assertion in the setup adapter needed a `// SAFETY:` line for
the assertion ratchet.
- `extensions/a2a/runtime-api.ts` needed registering in the runtime-api
classification list, like every other bundled channel barrel.
Also repairs a pre-existing incomplete `vi.mock` factory for `../infra/fs-safe.js`
in the agent-delete suites. Both pass in isolation, but the shared-worker lane
lets a sibling importer reach `readLocalFileSafely`, which the partial factory
never exported; adding this channel changed shard composition and surfaced it.
The factory now spreads the real module and overrides only `movePathToTrash`.
* chore(a2a): refresh config baseline for the configWrites surface
* chore(tests): adopt main's fs-safe mock repair for the agent-delete suites
Main landed the same importOriginal spread for the shared-worker mock-defeat
while this branch carried an equivalent fix; take main's version verbatim so the
two do not conflict.
* fix(a2a): isolate peer sessions and stop faking task cancellation
Addresses both ClawSweeper P1 findings.
Peer sessions: inbound routing never passed `dmScope`, so it fell back to
`session.dmScope ?? "main"` and every authenticated peer converged on
`agent:main:main` under default config. Untrusted remote content was joining the
operator's own session, and peers shared conversation history. A2A now pins
`per-account-channel-peer`; the peer id already embeds the A2A contextId, so each
peer+context pair gets its own session regardless of global session config.
Cancellation: `CancelTask` marked the task `TASK_STATE_CANCELED` and discarded
the late reply, but the dispatched agent run kept going and could still use
tools. The peer was told the work stopped when it had not. There is no
plugin-facing seam to abort a live run, so cancellation is refused with -32004
instead of acknowledged, and the store's cancel path plus its canceled-delivery
tombstone are deleted rather than left as a lying terminal state. Documented as
an explicit limitation.
Live-verified on a real gateway: 34/34 proofs, including three distinct
`agent:main:a2a:default:direct:<peer>:<context>` sessions with zero traffic in
`agent:main:main`, and both cancel spellings refused while the task stays
WORKING. Re-ran the official a2a-sdk 1.1.2 Docker interop afterwards: 9/9.
* feat(msteams): deliver native Adaptive Card approvals
Exec and gateway plugin approvals now render as Adaptive Cards in Microsoft
Teams with token-bound approve/deny actions, mirroring the Google Chat card
pattern. Card submits are intercepted before message-text serialization,
authorized against channels.msteams.allowFrom/defaultTo AAD object IDs via
the existing approval auth, claimed once, resolved over the gateway, and the
card is updated in place to its terminal state. Native delivery gates on the
top-level approvals.exec/approvals.plugin forwarding config; the /approve
text fallback remains.
* chore(msteams): shrink assertion-safety baseline after send.ts cast removal
* chore(msteams): record approval-native adapter seam in chained-assertion ledger
* fix(msteams): surface a text approval fallback when card delivery fails
When the native route suppressed the local text prompt, a failed Adaptive
Card send only logged, leaving the pending approval invisible. On delivery
error, send a plain-text /approve prompt to the planned target so the
operator always has a visible approval path. Addresses the ClawSweeper P1
on #129997 channel-locally; #130040 tracks the shared-boundary fix.
* docs(channels): scope implicitMentions overrides to the channels that read them
Only Mattermost, Slack, and Tlon call resolveChannelImplicitMentions, and they
are the only channels whose schemas accept the key. The other documented
producers pass no policy, so allowedImplicitMentionKinds stays undefined and
every produced fact counts as a mention.
* docs(channels): correct implicitMentions override wording
QQBot is a listed reply-to-bot producer with a passthrough schema, so the key
is accepted and ignored there rather than rejected. Also drops 'yet', which
implied a planned rollout while #80234 is still an open product decision.
* docs: limit implicit mention claims to bundled channels
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Matrix gains emoji-list backed by MSC2545 image packs: room state
im.ponies.room_emotes (any state key) plus the personal
im.ponies.user_emotes account-data pack, sticker-only entries excluded,
identifiers are literal shortcodes (Matrix reactions send the annotation
key as-is; the mxc URL is exposed alongside). Discord emoji-list results
now ride the lifecycle-owned bounded entity cache, invalidated by
GuildEmojisUpdate; the non-privileged GuildExpressions intent is added so
that dispatch actually arrives. Google Chat custom-emoji listing is
documented as unavailable: customEmojis.list requires user auth while the
plugin is a chat.bot service-account channel.
* docs(channels): record the WhatsApp ack-reaction exceptions
The shared reference promises channel and account ackReaction overrides plus an
identity fallback for every channel, but only Discord, Matrix, Slack, and
Telegram accept those keys, and WhatsApp sends no acknowledgment at all when
messages.ackReaction is unset. Also notes that group activation always bypasses
the group-mentions mention check.
* docs(whatsapp): qualify the ack-reaction exception
The eligibility path reads the account-aware channels.whatsapp.reactionLevel
before the message settings, so "only" applies to the emoji and scope rather
than to the whole decision.
* docs(whatsapp): describe supported ack reaction overrides precisely
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(channels): custom emoji discovery via emoji-list across Discord, Slack, Telegram
Make custom emojis discoverable by the agent. The message tool's emoji
param now documents custom-emoji syntax per channel (gate-aware, only
naming emoji-list when the action is actually advertised). Discord
emoji-list defaults guildId from the current conversation and returns
reaction-ready { name, identifier, animated? } entries; Slack returns
normalized shortcodes with aliasOf. Telegram gains emoji-list backed by
one canonical allowed-reactions owner (getChat available_reactions,
custom_emoji entries preserved), numeric custom-emoji reactions, and
replaces the dead 'reaction disallow list' error advice with a bounded
sample of the chat's allowed reactions.
* test(channels): expect telegram emoji-list provider-owned read gate in plugin shape contract
* test(telegram): prove emoji-list authority chain via mock-gateway e2e
Ephemeral gateway + mock Bot API + mock OpenAI provider: current-chat
emoji-list returns normalized standard and custom_emoji identifiers with
exactly one getChat call; a delegated cross-chat request is rejected with
the conversation-binding error and zero Bot API requests reference the
foreign chat.
* fix(matrix): recognize room version 12 room IDs (no :server suffix)
Room version 12 (MSC4291) dropped the trailing ":server" from room
IDs -- they are now a hash of the create event. Every place in the
Matrix plugin that treated "!" + ":" as the signature of an
already-resolved room ID silently discarded valid v12 room IDs as
unresolved instead of using them directly:
- channels.matrix.rooms config resolution (config.ts) dropped the
entry entirely, so group rooms could never pass the groupPolicy
"allowlist" gate on a v12 homeserver -- messages were dropped with
no reply and no default-level log line, since the only trace is a
verbose-only debug log.
- The invite auto-join allowlist validator and the interactive
group-room setup resolver in onboarding.ts had the same check
duplicated, so a user typing a v12 room ID during setup would be
told it was invalid.
- session-route.ts's per-room DM recipientSessionExact check had the
same gap for room-kind sends.
Runtime auto-join (auto-join.ts) already only checked for the "!"
sigil, which is why joining a v12 room worked while responding in it
did not -- this made the bug hard to spot from the join path alone.
Fix: add a single canonical isMatrixRoomId predicate next to the
existing isMatrixQualifiedUserId in target-ids.ts (user IDs and
aliases still require ":server" per spec; only room IDs changed) and
reuse it at all four sites instead of repeating the stale check.
Confirmed live against a real Room v12 homeserver (Conduit): the
server's own /joined_rooms response returns bare "!<hash>" room IDs
with no colon.
* docs(matrix): document suffixless room version 12 room IDs
Room version 12 (MSC4291) dropped the ":server" suffix from room
IDs. Document that the suffixless "!room" form is accepted anywhere
the docs previously only showed "!room:server", matching the
target-ids.ts fix landed in this same PR.
* fix(matrix): update stale Room v12 guidance text
* docs(matrix): accept suffixless Room v12 IDs in the group-policy guide
docs/channels/matrix.md already documents that channels.matrix.groups
accepts the suffixless !room form on room version 12+, but the
group-policy guide (docs/channels/groups.md) still only listed
!room:server, contradicting the channel doc an operator on a v12
homeserver would actually be following.
Addresses the ClawSweeper P2 finding on PR #123931.
* fix(matrix): advertise suffixless Room v12 IDs in onboarding placeholders
The invite auto-join and group-room setup prompts' placeholder text
still showed only `!roomId:server`, even though the retry note,
validation, and unresolved-room diagnostic already accept and describe
the suffixless `!roomId` form on room version 12+. An operator on a
v12 homeserver would see their homeserver's own room IDs contradicted
by the very placeholder guiding them through setup.
Updated both placeholders to list the suffixless form alongside the
existing examples, matching the phrasing already used in
docs/channels/matrix.md and the invite retry note. Added
configureRoomsAccess/roomsAllowlist options to the shared
createMatrixUpdateKeepCredentialsPrompter test harness (mirroring the
existing inviteAutoJoin option) so the group-room setup flow can be
exercised without duplicating the base prompter setup, then added
focused tests asserting the exact placeholder text for both prompts.
Verified both new tests fail against the pre-fix placeholders and pass
after.
Addresses the two ClawSweeper P2 findings on PR #123931.
* test(matrix): restore only allowlisted environment keys
* fix(matrix): reject empty room identifiers
---------
Co-authored-by: Patrick Erichsen <patrick.a.erichsen@gmail.com>
* feat(discord): auto-join occupied voice rooms
Add opt-in voice.autoJoin[].whenOccupied residency so Discord voice bots join for the first human and leave when the room becomes empty while preserving existing always-on, manual, transcript, and follow-user behavior.\n\nCloses #125973
* test(discord): isolate process runtime mocks
Use stable hoisted runtime-env mocks so isolate=false Discord test ordering cannot turn sleepWithAbort back into an unmocked function.
* fix(discord): defer unknown voice occupancy
Treat memberless voice states as unresolved instead of human so bot-only rooms cannot trigger occupancy-managed auto-join. Add cache-to-listener and manager regressions.\n\nCloses #125973
* test: isolate shared module mocks
Replace ineffective non-isolated module spies with stable hoisted mocks and a child-process SQLite connection-reuse probe so gateway and Discord shards are order-independent.
* test(gateway): inline connection reuse probe
Keep the child-process SQLite ownership probe in its owning Vitest file so Knip sees the full test surface without weakening process isolation or the original order regression.
* fix(gateway): bind auth limits to ingress attribution
* fix(gateway): close remaining ingress auth gaps
* fix(gateway): carry attribution into new ingress paths
* fix(gateway): close ingress ownership gaps
* fix(gateway): complete proxy ingress hardening
* fix(gateway): stabilize managed Tailscale ingress
* fix(gateway): make Tailscale cleanup ownership-safe
Refuse reset-on-exit publication until Tailscale exposes an atomic owner-bound cleanup operation, and migrate legacy configs with Doctor.
* fix(gateway): finish ingress ownership repair
* fix(gateway): own managed Tailscale route lifetime
Run managed Serve and Funnel routes as foreground claims tied to the Gateway lifecycle. Retire named Service config through Doctor because Tailscale Services cannot run in foreground mode.
Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>
* fix(gateway): align Tailscale consumers and build guards
Remove the retired named-service config from Telegram Mini App URL resolution and register the lifecycle worker as an explicit production entry.
Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>
* fix(gateway): preserve retired Tailscale inputs
Keep Funnel enabled when removing an ignored named-Service setting and accept the legacy positive reset flag as a no-op now that managed routes always follow Gateway lifetime.
Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>
* fix(gateway): preserve Tailscale route diagnostics
Prefer the actionable foreground CLI failure captured during timeout cleanup, and cover the original delayed-failure ordering.
Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>
* fix(gateway): reconcile Tailscale ingress with main
Preserve current ingress ownership contracts after the rebase, retire the obsolete device-auth migration check, validate route-owner IPC, and move Tailscale auth coverage onto the managed listener.
Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>
* fix(gateway): finish ingress rebase coverage
Unify the rebased net imports and let module-reset WebSocket tests prepare attribution through the same fresh module instance as the handler.
Co-authored-by: Pavan Kumar Gondhi <pavangondhi@gmail.com>
* test(gateway): align run-loop server fixture
---------
Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
* refactor(line): replace nine-marker prompt DSL with typed rich messages
Delete the LINE plugin's double-bracket marker language (quick_replies,
location, confirm, buttons, media_player, event, agenda, device,
appletv_remote) and its parser. Portable interactions now flow through the
existing presentation-block seam (renderPresentation, matching Discord and
Feishu); LINE-specific cards ride closed channelData.line schemas mapped to
the existing Flex renderers. Prompt section shrinks to four capability
lines and explicitly de-fangs marker text. Removes the stale
assertion-safety baseline entry for the deleted parser.
Production LOC net -69, tests net -433. Suite: 510/510 green.
* fix(line): declare rich message schema dependency
* fix(line): satisfy rich message type checks
* docs(line): mark card fragments as partial
Reject reserved /healthz webhook configurations before registration, preventing silent Telegram update loss. Warn through Doctor and document recovery.
Co-authored-by: Peter Lee <22994703+xialonglee@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* feat(slack): unify the native progress turn into one streamed message
Native progress mode now streams the whole turn into a single Slack message:
narration flows as markdown_text chunks interleaved with plan/task chunks,
task rows carry file-path details and +N/-N output, the terminal task links
the session via url_source, and the final answer lands through stopStream in
the same message. Media/oversized/error finals keep their normal-delivery
fallbacks.
Deletes the finished-card receipt collapse (the '\u{1F6E0} N tool calls · ⏱ Ns' edit)
outright: the card now stays as its finished self.
Live-verified on a real workspace: exactly one bot message per turn.
* fix(slack): serialize native stream updates and keep append-only rendered text monotonic
Overlapping progress updates (compositor render, narration payload, final)
computed their narration delta before awaiting the network and committed
state only afterwards, so concurrent updates re-appended identical
narration into the streamed message (each status line landed 3x live).
A single ordering chain now makes each update's compute -> append -> commit
atomic.
applyAppendOnlyStreamUpdate also replaced the accumulated rendered text
with the incoming cumulative partial once an appended chunk had diverged
rendered from source, dropping content the sink already displayed; rendered
now only ever extends.
* fix(slack): stop refreshing thread status once a turn has visible output
Slack clears the assistant thread status as soon as the app puts anything
in the thread, and renders its own rotating agent-working row ("Generating
response...", "Finding answers...") for every status write after that -- it
ignores the app-supplied string. The typing keepalive re-set the status
every 3s for up to 60s, so each turn painted a duplicate status row under
the streamed card or progress message.
The status write is now gated on the turn having visible output, which the
dispatcher already tracks (delivered reply, committed preview, or posted
draft message). The first status still fires before any output, so slow
turns keep their indicator, and the typing reaction is tracked separately
so a suppressed status write still cleans up its reaction.
* fix(slack): let the plan card own the status line instead of echoing it
The status headline and plan explanation fed both the streamed narration
markdown and the plan card title, so every headline rendered twice: once as
static text and once in the card that keeps updating it in place. Narration
now carries only authored commentary and reasoning, and a preamble payload
whose text the card title already shows is not streamed again.
* feat(slack): make the native agent card the default progress surface
Slack's native plan/task card was opt-in behind streaming.progress
.nativeTaskCards while the Block Kit session card shipped as the default.
The native surface is the better product on every axis we can measure --
one streamed message instead of three artifacts, live task rows with file
paths and diff counts, and Slack's own agent chrome -- so it becomes the
default and an explicit false selects the Block Kit card instead.
The session link is now emitted only when it can actually work: the
operator set gateway.publicOrigin and left the Control UI enabled.
Installations with no externally reachable Gateway get no link rather than
a dead one.
The progress card still only appears for turns that do real work; the
existing compositor start gate keeps plain question-and-answer turns
card-free.
* fix(slack): finish the final inside a buffered native stream
A short narration leaves the SDK session un-flushed, so `delivered` stays
false until `stop` makes its first network call. Requiring delivery before
finishing in-stream sent the final through normal delivery and then
finalized the stream anyway, producing exactly the second message this path
exists to prevent. Stop-time rejection already falls back via
SlackStreamNotDeliveredError, so a live session is enough.
Addresses the ClawSweeper P1/P2 finding on this PR.
* refactor(slack): collapse duplicate streaming surfaces and drop dead code
Cleanup pass over the progress/streaming neighborhood, all verified unused
by exhaustive reference search:
- Deleted buildSlackProgressStreamStartChunks/UpdateChunks: byte-identical
pass-throughs to the same builder, plus the render-module branch that
chose between them. One exported builder now.
- Collapsed slackStreaming.draftMode, a lossless restatement of the mode it
was derived from, and its outbound mapper; nine comparisons now read the
mode directly. Inbound legacy parsing stays for doctor migration.
- Dropped stopSlackStream's text parameter, the draft stream's stop() member
and onMessageSent hook, a redundant nativeStreaming argument, four dead
members on the progress runtime, and two single-expression wrappers.
- Deduped the native card title, which was computed twice per render.
Production LOC for the whole PR drops from +216 to +114.
* chore(config): regenerate bundled channel metadata for the Slack card default
The generated metadata still carried the old opt-in help text and
default-false description for streaming.progress.nativeTaskCards, so
config UI and diagnostics would publish stale guidance.
* fix(slack): un-export the now-internal legacy draft-mode type
Collapsing draftMode removed the type's only external consumer, so knip
flagged it as an unused export. Doctor migration still parses these legacy
values inbound, so the type stays module-local.