* refactor(channels): introduce ExternalThreadId newtype at channel boundary
External channel thread ids (Telegram chat id, web UUID, Slack thread_ts)
flow as raw Option<String> through IncomingMessage, StatusUpdate, and
pending-gate store. Wraps them in a validated ExternalThreadId so the
compiler distinguishes boundary-layer ids from the internal ThreadId(Uuid).
Maps to bug pattern from #2349, #2444, #2517 where thread-id confusion
crossed a layer silently.
* fix(bridge): adapt test thread_id to ExternalThreadId newtype
Post-merge fix: a test added in staging (insert_and_notify_pending_gate_uses_extension_manager_for_auth_display_name) assigned a raw String to message.thread_id, but the field type became ExternalThreadId on this branch. Wrap with ExternalThreadId::from_trusted to match the other tests in the same module.
* refactor(types): address review feedback — byte units, shared validate, try_-variants, dedup pending-gate
* refactor(types): validate scope_thread_id + relay respond prefers typed msg.thread_id
- router.rs: scope_thread_id written to PendingGate was wrapped via
ExternalThreadId::from_trusted from message.conversation_scope(), which
can carry untrusted WASM/metadata-sourced strings. Now validates via
ExternalThreadId::new; invalid values log at debug and store None.
Applied at both call sites (authentication-fallback path and generic
gate-insertion path).
- relay/channel.rs: respond() derived thread_id only from response or
metadata — now also consults the validated msg.thread_id as the second
fallback (before raw metadata) and filters empty strings so we never
emit thread_ts: "" to Slack.