* 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>
Keep one-message replacements whole through existing rich-to-plain recovery instead of selecting only the first send page. Preserve send pagination, preview clamping, and visible oversized-edit failures.
Fixes#130525
* fix(outbound): terminalize definitive channel rejections
* refactor(outbound): rethrow unclassified Slack rejections by identity
The Slack send boundary replaced every non-Error rejection with a synthetic
Error before classifying. That changed the propagated value at all four send
call sites, contradicting the stated contract that unlisted rejections keep
their previous path, and forced a compensating one-level `cause` walk in
`isSlackInvalidBlocksError` so the downstream `invalid_blocks` fallback could
still match. The guard's second operand was also dead: `isRecord` accepts Error
instances, so `!(err instanceof Error) || !isRecord(err)` never reached its
right side for a plain object.
Classify off the raw value and rethrow unclassified rejections by identity;
the `cause`-walking compensator and its test go away with it. Distill the
Telegram migration classifier's three-state result object into a
message-or-nothing, and document the description-first and 52-bit id contracts
inline.
Production surface for the PR drops from +71/-11 to +62/-8.
* test(agents): remove clock-tick race from workspace bootstrap ctime coverage
The in-place-edit case added in #127769 assumes restoring mtime leaves ctime as
the only changed stat field, and that it therefore differs. Linux and macOS
stamp ctime from a coarse per-tick clock, so an edit landing in the same tick as
the cached stat leaves ctimeMs equal: the ctime-only scenario never occurs, the
cache correctly serves its entry, and the assertion fails. Measured 199/200
identical ctimes in a tight loop; the test failed 1 in 8 local runs and broke
checks-node-compact-large-14 on CI.
Re-touch until the kernel advances ctimeMs, then assert mtimeMs and size are
unchanged so the scenario is provably ctime-only. Stripping ctimeMs from the
cache identity still fails the test.
* fix(slack): keep post-dispatch upload completion rejections ambiguous
PlatformMessageNotDispatchedError is a provider assertion that no
recipient-visible send began, and its contract says never use it after an
ambiguous send. files.completeUploadExternal runs after onPlatformSendDispatch
and is the one-time share operation, so a rejection there cannot prove the file
was never shared however definitive its code reads.
Drop the permanent-rejection classifier from that call and keep it on the
pre-dispatch calls only (chat.postMessage, files.getUploadURLExternal,
resolveChannelId). The upload test that pinned the old behavior asserted
onPlatformSendDispatch had already fired, which is exactly the condition that
forbids the claim; it now pins ambiguity instead.
Also widens the workspace bootstrap ctime wait to a 1s deadline and reshapes it
as a while loop, so a coarser filesystem tick cannot exhaust the bound.
* test(slack): prove permanent rejection recovery
* fix(test): stabilize Slack channel action routing
* fix(test): retain channel parity for precise targets
* refactor(outbound): drop unrelated test-routing changes
* fix(telegram): require Bot API error code for migration rejection
* test: repair Telegram tuples and preserve Slack test routing
---------
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* feat(approvals): typed approval scope summaries on channel cards
Approval owners can attach a closed ApprovalScope union (message-send,
payment, external-post) describing an action's blast radius. The gateway
sanitizes it once at the producer boundary, the core view model renders a
Scope metadata row so Slack/Discord/Google Chat cards show it unchanged,
shared text builders cover Telegram/WhatsApp/Signal/iMessage/Matrix, and
the durable presentation carries it additively for operator surfaces.
Scope is display-only, never authorization; missing scope keeps today's
cards.
* fix(approvals): emit native ApprovalScope union and clamp recipient previews
Name the three scope variants as registered protocol schemas so the Swift
generator emits the ApprovalScope discriminated union the presentation
structs reference, and commit the regenerated GatewayModels.swift. Clamp
recipient previews to the declared recipientCount at the sanitize boundary
so a count of 1 with 2 previews can no longer render inconsistently.
Addresses both ClawSweeper findings on #130116.
* refactor(approvals): extract text sanitizer to break the exec-approvals import cycle
check:architecture flagged approval-scope joining the exec-approvals SCC
through exec-approval-command-display. Move the self-contained display
sanitizer into a leaf module (exec-approval-text-sanitize) with no
exec-approvals imports and migrate all sanitize importers; command-display
keeps only the payload-typed command/preview resolver.
* chore(plugin-sdk): ratchet public surface budgets down after sanitizer extraction
The approval display sanitizers left the publicly reachable SDK graph when
they moved to the exec-approval-text-sanitize leaf: exports 4343 -> 4338,
callable exports 2582 -> 2578. Shrink-only budget pin.
Telegram progress streaming treated buttonless ask-user replies as transient progress, so multi-question prompts could disappear without a visible delivery. Keep ask-user payloads on the durable question path and cover the owner boundary with a regression test.
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Requeue Telegram ingress claims when their owner aborts before adoption, without spending retry attempts or misclassifying late aborts as delivery failures. During managed shutdown, drain active embedded model and tool runs before exit, with a 330-second generated systemd stop budget for the 315-second cooperative gateway drain.
Co-authored-by: jisubi <jisubi@gmail.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Use the cached Telegram forum flag to keep General-topic updates on their `topic:1` session lane during admission and durable-row reconciliation. This prevents the session-init retry loop while preserving explicit forum hints.
Co-authored-by: datenpapst <94785785+datenpapst@users.noreply.github.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
Preserve rich Telegram responses by resolving the account limit before outbound chunking, while keeping explicit legacy HTML at the existing 4,000-character limit.
Co-authored-by: gaoanze <gaoanze@meituan.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* fix(reply): redact private prompt context before delivery
Protect final and streaming replies from exact copied inbound prompts, including same-line, fenced, quoted, and partial streaming variants. Preserve safe literal examples and suppress empty exec-output placeholders. Fixes#78177.
Co-authored-by: SnowSky1 <3060176569@qq.com>
* test(telegram): own reply privacy delivery integration
Move real Telegram HTTP privacy regressions into the plugin-owned test graph and drive finalized inbound dispatch through the public SDK.
Co-authored-by: SnowSky1 <3060176569@qq.com>
* fix(reply): redact multiline quoted private prompt context
Recognize standard, indented, and nested Markdown blockquotes in final replies and withhold every incremental quoted streaming prefix before private history can reach Telegram.
Co-authored-by: SnowSky1 <3060176569@qq.com>
* fix(reply): redact decorated private prompt streams
Match exact prompt-owned Markdown formatting per line and withhold every streamed prefix, including headings, indented code, list continuations, mixed wrappers, repeated markers, and safe same-line text.
Co-authored-by: SnowSky1 <3060176569@qq.com>
* fix(reply): redact carriage-return prompt copies
Accept every source-normalized line-ending variant before private context reaches final replies, streaming, or Telegram delivery.
Co-authored-by: SnowSky1 <3060176569@qq.com>
* fix(reply): bound streamed private prompt candidate scans
Reject non-marker candidates before allocating suffixes and fail closed on repeated verified marker floods to prevent quadratic streaming work.
Co-authored-by: SnowSky1 <3060176569@qq.com>
* fix(reply): keep prompt-line character extraction type-safe
Use the canonical non-empty source-line character once so strict core, test, lint, and plugin boundary compilers agree without changing privacy behavior.
Co-authored-by: SnowSky1 <3060176569@qq.com>
---------
Co-authored-by: SnowSky1 <3060176569@qq.com>
* fix(telegram): route commentary through one frozen progress owner
Telegram progress mode enabled both the commentary-capable draft and
durable commentary payloads without supplying the shared
shouldDeliverCommentaryPayloads owner callback, so core defaulted
durable delivery on while the draft also rendered each completed
preamble (#128038). Wire the callback to the same verbose-visibility
slot core freezes at turn start, gated exactly when the compositor can
render interleaved commentary lines, mirroring the Slack/Discord
wiring from #121009. Commentary-disabled configs keep today's
durable-only path unchanged.
Closes#128038
AI-assisted (Claude); regression tests fail pre-fix and pass post-fix.
* test(telegram): cover commentary ownership across streaming modes
---------
Co-authored-by: Parker Fawcett <Parkerscottfawcett@gmail.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Failed channel turns could leave caller-owned pending group history in the next prompt. Finalize it on every terminal path, and filter Telegram rolling context while preserving explicit reply ancestry.
Co-authored-by: SunnyShu0925 <shu.zongyu@xydigit.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>
* 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.
A message-tool send that failed with proven-not-sent evidence (pre-connect
ECONNREFUSED/ETIMEDOUT, PlatformMessageNotDispatchedError) was retried twice:
the model resent it after seeing the error, and the durable outbound queue
row stayed replay-eligible, so the reconnect/poll drain sent it again.
Record retry ownership at the two producers of model-authored sends and
dead-letter caller-owned rows in the queue executor:
- in-process sends derive `deliveryRetryOwner: "caller"` from
`actionOrigin: "message-tool"` in message-action-send;
- gateway-owned plugin sends (telegram runs `message.action` in gateway
mode) derive it from the signed agent-runtime turn context
(`runtimeAgentId`) in the gateway handler and forward it through
ChannelMessageActionContext into the plugin's durable send.
Reporting-only callers (CLI, RPC clients, cron reusable intents) carry no
owner and keep recovery's replay (#100979). Remote gateway mode mints no
runtime identity and keeps the replayable row; tracked as a follow-up.
Fixes#124279
Co-authored-by: synthclaw <synthalorian@gmail.com>
Co-authored-by: Ayaan Zaidi <hi@obviy.us>