You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
service-messaging: the e-mail channel reports available on a LogTransport, so a send with no mail transport reads as delivered while nothing is sent (objectstack-ai/cloud#2549 ruling B, item ③) #30510
Filing gate: executing a recorded ruling. The maintainer ruled objectstack-ai/cloud#2549 B; their selection, verbatim, is 「B 只让租户自配邮箱」 (ruling record on that card). This card is item ③. Item ② (approvals request e-mail) is a sibling card.
Owner. The repo:cloud lane (repo:cloud#3, #30278) follows it to close; triage routes it. Priority p3, inherited from objectstack-ai/cloud#2549.
packages/services/service-messaging/src/email-channel.ts ~:341-345: isAvailable() is true whenever an email service object exists, otherwise {available:false, reason:'transport_not_configured'}. It does not ask which transport the service holds.
packages/plugins/plugin-email/src/email-plugin.ts: provider defaults to 'log' (:456). With none configured it logs "no transport configured — using LogTransport (mail will NOT be sent)" (:494). It binds to the mail settings namespace on kernel:ready and hot-swaps the transport (~:615-652).
In cloud, every hosted tenant kernel force-mounts email (apps/objectos/hosted-slate.ts:92) without a transport (packages/objectos-runtime/src/capability-loader.ts:524), so every hosted environment is in this state until its admin configures mail.
Not measured: an actual send on a tenant kernel. The "reports success, writes sys_email, delivers nothing" behaviour is quoted from cloud's capability-loader.ts comment (~:1022-1027).
Expected
plugin-email exposes whether its current transport is real (not the LogTransport), in a form the channel can read.
email-channel.isAvailable() answers {available:false, reason:'transport_not_configured'} while the transport is the LogTransport. It flips back without a restart when mail settings hot-swap a real transport in.
Acceptance
A test proves a kernel with no mail settings has the e-mail channel unavailable, with that reason, and no fake delivery row.
A test proves that after a settings hot-swap to a real transport the channel is available.
The existing messaging tests stay green.
Dedupe (2026-11-05): repo:objectstack-ai/objectstack email channel LogTransport available is:issue → 3 hits (QA runs only); transport_not_configured is:issue → 4 hits, all closed (#18567, #18050, #17732), none on the LogTransport case.
Executes item ③ of the maintainer's ruling B on objectstack-ai/cloud#2549. The e-mail channel reports itself available on a LogTransport, so a send with no mail transport reads as delivered while nothing is sent.
Claim: PM loop round 3
Session: session_01Utp6CXCS98uhsFQYk64epE (seat) · dev = project card thread session_01Utp6CaGmRyZ3XL5MuUgEZg
Account: os-bill
Branch: claude/project-thread-cufcnh (harness-pinned thread branch; platform exception to claude/issue-<n>-<slug>)
Worktree: objectstack-issue-30510
Domain: domain:server
Seat: domain:server#6
File surface: packages/services/service-messaging/src/** (e-mail channel and its composition), packages/plugins/plugin-email/src/** except mail.manifest.ts, their tests; content/docs/** only for sentences this PR makes false (not releases/**); one changeset. ⛔ packages/spec/**, ui/**, skills/**, docs/adr/**, .claude/**, AGENTS.md, **/mail.manifest.ts (stop on breach; explain in the report)
Container & model: M, mode:thread, model: default tier (opus), effort: high
Clause-②: no (provisional) — a channel that answered available on a LogTransport answers unavailable with the existing transport_not_configured reason; the dev corrects it from the diff
Metadata-protocol: none (provisional) — no authored metadata changes meaning
Responsibility: service-messaging email-channel.ts isAvailable() answers available whenever an email service exists, including one holding plugin-email's LogTransport fallback, so every e-mail send reads as delivered while nothing is sent | none: availability asks only whether the service exists | every deployment and hosted tenant with no mail transport configured (cloud#2549 ruling B item ③)
Thread-read: 6659943585
Premise: holds (seat read on origin/main: email-channel.ts:341 returns available: true when getEmail() resolves; email-plugin.ts:455-456 builds a LogTransport when provider is 'log', the default).
Serial constraints cleared: no open PR touches email-channel.ts or email-plugin.ts at 14:22Z; seat 5's #30212 row 3 edits mail.manifest.ts (excluded) and EmailService's default sender — whichever merges second merges origin/main; sibling #30509 (seat 6) stays off both files
Order: project file server-seat6/dispatch-30510.md, delivered to the card thread.
{
"issue": 30510,
"status": "done",
"branch": "claude/project-thread-cufcnh",
"pr": "https://github.com/objectstack-ai/objectstack/pull/30547",
"session": "session_01Utp6CaGmRyZ3XL5MuUgEZg",
"premise_still_valid": true,
"summary": "Premise re-measured on origin/main fa9c0aa2: email-channel.ts isAvailable() answered available whenever getEmail() resolved; plugin-email's EmailService already implements the spec's optional IEmailService.isConfigured() from the live transport (false on LogTransport, follows setTransport hot swaps, pinned in email-plugin.mail-settings.test.ts). So no spec and no plugin-email change was needed: the channel now asks isConfigured() on every call (emailTransportRefusal); an explicit false answers {available:false, reason:'transport_not_configured'} at fan-out and refuses send() with the same reason, graded permanent, without calling the service. A service without isConfigured keeps today's behaviour (available), declared in Acceptance notes and open_questions.",
"tests": "head fbfbaf47. Red on fa9c0aa2 (tests only): email-channel.test.ts 'Tests 4 failed | 43 passed (47)'. Green: 'Tests 47 passed (47)'. Ablation (scripts/ablation-replace.mjs, emailTransportRefusal forced to undefined; anchor 1->0, blob deebc1c7be23->a81f47328d2c): 'Tests 4 failed | 43 passed (47)'; restored, blob == HEAD, git diff HEAD empty; tests import src by relative path, no dist between. Package suite under os-verify-lock: 'Test Files 66 passed (66) · Tests 817 passed (817)', VERDICT command-exit 0. typecheck exit 0 (--listFiles includes email-channel.test.ts). eslint --no-inline-config on both changed files exit 0. check:nul-bytes exit 0.",
"gates": "dispatch-gates --commands on fbfbaf47: 62 commands, each run with exit captured before any pipe; --ran: '62 derived, 59 run, 3 NOT-MEASURED, 0 UNRUN'. NOT MEASURED (exit 3 prerequisite): pnpm check:dual-build-cjs-loads (whole workspace not built), pnpm check:i18n (build closure missing; no translation bundle touched), node scripts/check-plugin-teardown-shape.mjs --self-test (shallow clone; checker health only). CI families: PR check-runs in_progress at report time.",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
"line_budget": "212 changed lines (+209/-3), 3 files; under the 3000 human-merge threshold",
"files_changed": [
".changeset/30510-email-channel-log-transport-unavailable.md",
"packages/services/service-messaging/src/email-channel.ts",
"packages/services/service-messaging/src/email-channel.test.ts"
],
"deviations": [
"Repo-wide `pnpm lint` not run locally (os-dev.md: repo-level scans are CI's Lint job); eslint --no-inline-config ran on the two changed source files instead, exit 0.",
"The order's kernel pin (plugin-email + messaging in one kernel) is pinned as two halves joined by the spec contract: service-messaging has no plugin-email dependency and dogfood is outside the file surface. The producer half (isConfigured false on the out-of-the-box boot, true after a saved SMTP config) was already pinned in plugin-email.",
"plugin-email untouched, so no overlap with seat 5's EmailService edit.",
"PR body carries the harness attribution comment as line 1 and the session-URL footer; harness-written commit trailers none (commits carry the model-free pair)."
],
"mcp_calls": "0",
"api_writes": "2 — relay pr_create (objectstack-ai/objectstack#30547, run 42051382947); post-stamped comment on #30510 (this report). Plus git push (not a REST write).",
"open_questions": [
{
"question": "An email service with no isConfigured member: available (unknown, today's behaviour) or unavailable (absent read as false, as plugin-auth's sign-up flow reads it)?",
"options": [
"A keep available: only an explicit false refuses; no host-supplied service that predates the member goes silent",
"B unavailable: matches the contract's 'never as true' sentence literally, but mutes every email service without the member"
],
"recommendation": "A, because the fan-out ruling already chose absent = available for a channel without isAvailable, the ruling here is about the LogTransport, and B is a separate muting decision; flipping is one line in emailTransportRefusal"
}
],
"security_findings": "none",
"out_of_scope_findings": []
}
State change: pm:dispatched removed; card closed; card thread resolved. Item ③ of ruling B on objectstack-ai/cloud#2549 is delivered here; item ② is #30509 (PR #30567, in review).
Activity
objectstack-fleet commentedon Nov 5, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: audit · p2 ·
domain:server· 2026-11-05T14:02Z⛔ Not a claim.
LogTransport, so a send with no mail transport reads as delivered while nothing is sent.service-messaging,plugin-email), the same routing as metadata-protocol: the publish receipt's seedApplied drops the landed row ids the seed loader computes, so cloud cannot tell which rows an AI build's seed landed on (objectstack-ai/cloud#3598 ruling A) #30348, andrepo:cloud#3follows it.Generated by Claude Code
objectstack-fleet commentedon Nov 5, 2026
domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE
Claim: PM loop round 3
Session:
session_01Utp6CXCS98uhsFQYk64epE(seat) · dev = project card threadsession_01Utp6CaGmRyZ3XL5MuUgEZgAccount:
os-billBranch:
claude/project-thread-cufcnh(harness-pinned thread branch; platform exception toclaude/issue-<n>-<slug>)Worktree:
objectstack-issue-30510Domain:
domain:serverSeat:
domain:server#6File surface:
packages/services/service-messaging/src/**(e-mail channel and its composition),packages/plugins/plugin-email/src/**exceptmail.manifest.ts, their tests;content/docs/**only for sentences this PR makes false (notreleases/**); one changeset. ⛔packages/spec/**,ui/**,skills/**,docs/adr/**,.claude/**,AGENTS.md,**/mail.manifest.ts(stop on breach; explain in the report)Container & model:
M,mode:thread,model: default tier (opus),effort: highClause-②: no (provisional) — a channel that answered available on a LogTransport answers unavailable with the existing
transport_not_configuredreason; the dev corrects it from the diffMetadata-protocol: none (provisional) — no authored metadata changes meaning
Responsibility:
service-messaging email-channel.ts isAvailable() answers available whenever an email service exists, including one holding plugin-email's LogTransport fallback, so every e-mail send reads as delivered while nothing is sent | none: availability asks only whether the service exists | every deployment and hosted tenant with no mail transport configured (cloud#2549 ruling B item ③)Thread-read: 6659943585
Premise: holds (seat read on
origin/main:email-channel.ts:341returnsavailable: truewhengetEmail()resolves;email-plugin.ts:455-456builds aLogTransportwhenprovideris'log', the default).Serial constraints cleared:
no open PR touches email-channel.ts or email-plugin.ts at 14:22Z; seat 5's #30212 row 3 edits mail.manifest.ts (excluded) and EmailService's default sender — whichever merges second merges origin/main; sibling #30509 (seat 6) stays off both filesOrder: project file
server-seat6/dispatch-30510.md, delivered to the card thread.objectstack-fleet commentedon Nov 5, 2026
domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE
os-dev-report
{ "issue": 30510, "status": "done", "branch": "claude/project-thread-cufcnh", "pr": "https://github.com/objectstack-ai/objectstack/pull/30547", "session": "session_01Utp6CaGmRyZ3XL5MuUgEZg", "premise_still_valid": true, "summary": "Premise re-measured on origin/main fa9c0aa2: email-channel.ts isAvailable() answered available whenever getEmail() resolved; plugin-email's EmailService already implements the spec's optional IEmailService.isConfigured() from the live transport (false on LogTransport, follows setTransport hot swaps, pinned in email-plugin.mail-settings.test.ts). So no spec and no plugin-email change was needed: the channel now asks isConfigured() on every call (emailTransportRefusal); an explicit false answers {available:false, reason:'transport_not_configured'} at fan-out and refuses send() with the same reason, graded permanent, without calling the service. A service without isConfigured keeps today's behaviour (available), declared in Acceptance notes and open_questions.", "tests": "head fbfbaf47. Red on fa9c0aa2 (tests only): email-channel.test.ts 'Tests 4 failed | 43 passed (47)'. Green: 'Tests 47 passed (47)'. Ablation (scripts/ablation-replace.mjs, emailTransportRefusal forced to undefined; anchor 1->0, blob deebc1c7be23->a81f47328d2c): 'Tests 4 failed | 43 passed (47)'; restored, blob == HEAD, git diff HEAD empty; tests import src by relative path, no dist between. Package suite under os-verify-lock: 'Test Files 66 passed (66) · Tests 817 passed (817)', VERDICT command-exit 0. typecheck exit 0 (--listFiles includes email-channel.test.ts). eslint --no-inline-config on both changed files exit 0. check:nul-bytes exit 0.", "gates": "dispatch-gates --commands on fbfbaf47: 62 commands, each run with exit captured before any pipe; --ran: '62 derived, 59 run, 3 NOT-MEASURED, 0 UNRUN'. NOT MEASURED (exit 3 prerequisite): pnpm check:dual-build-cjs-loads (whole workspace not built), pnpm check:i18n (build closure missing; no translation bundle touched), node scripts/check-plugin-teardown-shape.mjs --self-test (shallow clone; checker health only). CI families: PR check-runs in_progress at report time.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "line_budget": "212 changed lines (+209/-3), 3 files; under the 3000 human-merge threshold", "files_changed": [ ".changeset/30510-email-channel-log-transport-unavailable.md", "packages/services/service-messaging/src/email-channel.ts", "packages/services/service-messaging/src/email-channel.test.ts" ], "deviations": [ "Repo-wide `pnpm lint` not run locally (os-dev.md: repo-level scans are CI's Lint job); eslint --no-inline-config ran on the two changed source files instead, exit 0.", "The order's kernel pin (plugin-email + messaging in one kernel) is pinned as two halves joined by the spec contract: service-messaging has no plugin-email dependency and dogfood is outside the file surface. The producer half (isConfigured false on the out-of-the-box boot, true after a saved SMTP config) was already pinned in plugin-email.", "plugin-email untouched, so no overlap with seat 5's EmailService edit.", "PR body carries the harness attribution comment as line 1 and the session-URL footer; harness-written commit trailers none (commits carry the model-free pair)." ], "mcp_calls": "0", "api_writes": "2 — relay pr_create (objectstack-ai/objectstack#30547, run 42051382947); post-stamped comment on #30510 (this report). Plus git push (not a REST write).", "open_questions": [ { "question": "An email service with no isConfigured member: available (unknown, today's behaviour) or unavailable (absent read as false, as plugin-auth's sign-up flow reads it)?", "options": [ "A keep available: only an explicit false refuses; no host-supplied service that predates the member goes silent", "B unavailable: matches the contract's 'never as true' sentence literally, but mutes every email service without the member" ], "recommendation": "A, because the fan-out ruling already chose absent = available for a channel without isAvailable, the ruling here is about the LogTransport, and B is a separate muting decision; flipping is one line in emailTransportRefusal" } ], "security_findings": "none", "out_of_scope_findings": [] }Report taken 2026-11-05T15:06Z.
objectstack-fleet commentedon Nov 5, 2026
domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE
ACCEPT → PR #30547 comment 6661956909 · 2026-11-05T15:30Z · head
fbfbaf47:全绿,已 ready + auto-merge。objectstack-fleet commentedon Nov 5, 2026
domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE
Landed · 2026-11-05T16:26Z · PR #30547
merged, merge commitede9e6bbca, merged at 2026-11-05T16:24Z through the merge queue (seat record 6661956909)closed·completedby the PR'sFixesState change:
pm:dispatchedremoved; card closed; card thread resolved. Item ③ of ruling B on objectstack-ai/cloud#2549 is delivered here; item ② is #30509 (PR #30567, in review).