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
Split out of #18050 by the triage seat, answering that card's
pm:retriageask ①. ⛔ 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
mainby 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 viagit grep origin/main, ⛔ not from a report and ⛔ not from the shared worktree:email-channel.ts:238-245isAvailable(){ available: false, reason: 'transport_not_configured' }— correctemail-channel.ts:252send()return { ok: true }; // capability not installed — no-op, like inbox w/o datasms-channel.ts:152send()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 throughisAvailable()— and answers the send path as though it had delivered.Why this is its own card and not a note on #18050
messaging-service.ts/messaging-service-plugin.ts. This one lands inemail-channel.tsandsms-channel.ts.send()still can.## Acceptance notesinstead" is which PR will touch this file? — [finding] The outbox fan-out writes a delivery row for a channel that is NOT registered — #17732's reported symptom, which ruling A does not reach #18050's successors are landed and no open PR touches these two files, so the fallback does not hold.Direction of failure — silent, which is the expensive half
⛔ What this card does NOT decide
⛔ Not prescribing the fix.⚠️ #18041 settled that an unmounted channel is REFUSED rather than suppressed, and #18050's boundary pin
{ ok: false, error: … }, a suppression record, and reusing thetransport_not_configuredreason thatisAvailable()already returns are all plausible, and they differ in whether the result is retried, dead-lettered or suppressed.channel-availability.test.tsholds 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: trueTriage dedupe performed (⛔ not deferred to the implementer): local title scan over 523 open
objectstackcards — 0 hits, controlmessaging|channelreturns 3, so the instrument is live; one targeted semantic search including closed — 2 results, #14372 (asys_email.errordescription) and #5773 (a settings-to-transport contract test), neither this defect. No duplicate found.Generated by Claude Code