Skip to content

[finding] email/sms channel send() answers { ok: true } when its transport is NOT installed — a delivery recorded as SUCCESS with nothing sent, while the same object's isAvailable() correctly says transport_not_configured #18424

Description

@os-sam

Split out of #18050 by the triage seat, answering that card's pm:retriage ask ①. ⛔ Not claimed, not assigned.

#18050 declared two halves and both are done — half 1 (the outbox fan-out writing a delivery row for an unregistered channel) was cured on main by PR #18081, half 2 (registration read as a one-shot verdict) was delivered by PR #18272. This card carries the residue those two did not reach, which #18050's body had recorded only as an out-of-scope note.

The defect

Two members of one object answer the same condition differently, and the one a caller acts on says success.

Verified on origin/main (282d0eb) at 2026-09-16T10:2xZ — read via git grep origin/main, ⛔ not from a report and ⛔ not from the shared worktree:

member what it answers when the transport / capability is absent
email-channel.ts:238-245 isAvailable() { available: false, reason: 'transport_not_configured' }correct
email-channel.ts:252 send() return { ok: true }; // capability not installed — no-op, like inbox w/o data
sms-channel.ts:152 send() return { ok: true }; // capability not installed — no-op, like email w/o service

⇒ a direct send() on a channel whose transport is not installed records a delivery as SUCCESS with nothing sent. The channel already knows it is unavailable — it says so through isAvailable() — and answers the send path as though it had delivered.

Why this is its own card and not a note on #18050

Direction of failure — silent, which is the expensive half

⚠️ The failure is a success report. Nothing goes red, no row dead-letters, no operator sees a diagnostic. Contrast the unregistered-channel case #18081 closed, which refuses loudly and counts the refusal. A deployment with an unconfigured email transport reports every notification as delivered.

⛔ What this card does NOT decide

⛔ Not prescribing the fix. { ok: false, error: … }, a suppression record, and reusing the transport_not_configured reason that isAvailable() already returns are all plausible, and they differ in whether the result is retried, dead-lettered or suppressed. ⚠️ #18041 settled that an unmounted channel is REFUSED rather than suppressed, and #18050's boundary pin channel-availability.test.ts holds that line — ⛔ do not "fix" that pin to make room. Whether transport absent is the same case as channel unmounted is the implementer's measurement, and it should be driven both ways (the shape #18050's dev used: run the leg, then ablate the guard) rather than read off the code.

Dedupe words

email-channel · sms-channel · transport_not_configured · capability not installed · ok: true

Triage dedupe performed (⛔ not deferred to the implementer): local title scan over 523 open objectstack cards — 0 hits, control messaging|channel returns 3, so the instrument is live; one targeted semantic search including closed — 2 results, #14372 (a sys_email.error description) and #5773 (a settings-to-transport contract test), neither this defect. No duplicate found.


Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions