From 1e8f66ef6e529028528eec39c93534871627c419 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Sat, 19 Sep 2026 10:03:21 -0700 Subject: [PATCH 1/3] test(web): a person's roster address is lowercase (CL-8614) --- apps/web/src/chat/threads-api.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/src/chat/threads-api.test.ts b/apps/web/src/chat/threads-api.test.ts index 79fd9d06d..5cda60a8e 100644 --- a/apps/web/src/chat/threads-api.test.ts +++ b/apps/web/src/chat/threads-api.test.ts @@ -67,7 +67,7 @@ describe("chatTitle", () => { }); describe("listWorkbenchParticipants", () => { - test("a person's address is their refId at the workbench's own domain, never email or bare refId", async () => { + test("a person's address is their refId lowercased at the workbench's own domain, never email or bare refId", async () => { globalThis.fetch = ((input: RequestInfo | URL) => { const path = typeof input === "string" ? input : String(input); if (path.includes("/principals")) { @@ -98,7 +98,7 @@ describe("listWorkbenchParticipants", () => { id: "prin_1", kind: "person", name: "Alice", - address: "Mk9tHH@example.com", + address: "mk9thh@example.com", }); }); }); From 6863cbc9fb24da1aecd8b32a562e778a801cab42 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Sat, 19 Sep 2026 10:03:21 -0700 Subject: [PATCH 2/3] fix(web,hub): derive a person's mail address in lowercase (CL-8614) --- apps/hub/src/server.ts | 4 +++- apps/web/src/agent-deploy.ts | 5 ++++- apps/web/src/chat/threads-api.ts | 3 ++- apps/web/src/mail-address.ts | 7 +++++++ 4 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/mail-address.ts diff --git a/apps/hub/src/server.ts b/apps/hub/src/server.ts index 52dd699cd..1a70c2af1 100644 --- a/apps/hub/src/server.ts +++ b/apps/hub/src/server.ts @@ -614,7 +614,9 @@ export async function createHubServer({ if (principalRow === undefined) { throw new Error(`no principal "${principal.principalId}" to address a mailbox sender as`); } - return `${principalRow.refId}@${tenantRow.domain}`; + // A person's refId is mixed case; a small model lowercases an address + // before replying, so the canonical stamped form is lowercase. + return `${principalRow.refId}@${tenantRow.domain}`.toLowerCase(); }, deliver: createMailboxDeliver({ app, persistMail: mailboxLookups.persistMail }), }); diff --git a/apps/web/src/agent-deploy.ts b/apps/web/src/agent-deploy.ts index a2531d31d..dd2ccd66b 100644 --- a/apps/web/src/agent-deploy.ts +++ b/apps/web/src/agent-deploy.ts @@ -13,6 +13,7 @@ import { } from "@corbits/myra/workflow-ids"; import { ensureAgentHubCredential } from "./agent-hub-credential"; +import { personMailAddress } from "./mail-address"; import { resolveExistingOffering } from "./onboarding/provider-connect-step"; import { isValidSlug, slugify } from "@/lib/slug"; @@ -300,7 +301,9 @@ async function resolveDeployerAddress( return undefined; } const parsed = SessionUserShape(body); - return parsed instanceof type.errors ? undefined : `${parsed.user.id}@${tenantDomain}`; + return parsed instanceof type.errors + ? undefined + : personMailAddress(parsed.user.id, tenantDomain); } /** Creates a `@corbits/cron` schedule row targeting a deployed agent by its diff --git a/apps/web/src/chat/threads-api.ts b/apps/web/src/chat/threads-api.ts index adca6eb44..6ef64ec9b 100644 --- a/apps/web/src/chat/threads-api.ts +++ b/apps/web/src/chat/threads-api.ts @@ -9,6 +9,7 @@ import { reportError } from "@corbits/error-sink"; import { agentSlugFromSourceAssetName } from "../agent-deploy"; import { listTopLevelRuns } from "../agents-api"; import { MYRA_SOURCE_CONFIG } from "../myra-source"; +import { personMailAddress } from "../mail-address"; import { mentionedAgents } from "./mentions"; import { appendRoster } from "./workbench-roster"; @@ -646,7 +647,7 @@ export async function listWorkbenchParticipants( id: principal.id, kind: "person", name: principal.displayName, - address: `${principal.refId}@${tenantDomain}`, + address: personMailAddress(principal.refId, tenantDomain), })); const agents = chatAgents.map((agent): WorkbenchParticipant => ({ id: agent.id, diff --git a/apps/web/src/mail-address.ts b/apps/web/src/mail-address.ts new file mode 100644 index 000000000..2c429b0e7 --- /dev/null +++ b/apps/web/src/mail-address.ts @@ -0,0 +1,7 @@ +// A person's address is their auth user id, which is mixed case. Small models +// lowercase an address before replying, so the canonical form is lowercase +// everywhere a person's address is shown, stamped, or handed to an agent. + +export function personMailAddress(refId: string, tenantDomain: string): string { + return `${refId}@${tenantDomain}`.toLowerCase(); +} From 2a8dfd1b68993d7513c537122498c487d1b03d3b Mon Sep 17 00:00:00 2001 From: Sawyer Date: Sat, 19 Sep 2026 10:03:21 -0700 Subject: [PATCH 3/3] docs: a person's mail address is lowercase (CL-8614) --- docs/chat-mail-threading.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/chat-mail-threading.md b/docs/chat-mail-threading.md index edcd3483f..09e6cb77f 100644 --- a/docs/chat-mail-threading.md +++ b/docs/chat-mail-threading.md @@ -37,6 +37,17 @@ at send time to narrow the `to` list to just those agents. The token stays in the body so the agent sees who was addressed, and the roster block is still appended in full, so a narrowed message can still be handed on. +## A person's address is lowercase + +A person's address is their auth user id at the workbench domain, and that +id is mixed case. Small models lowercase an address before replying, so +`personMailAddress` lowercases it at every point the client derives one — +the roster, the Participants panel, a schedule's body — and the hub stamps +the same lowercase form on a person's outbound mail. Mailbox delivery +matches the local part case-insensitively, so both forms reach the same +inbox: threads written before this keep their mixed-case addresses, new +mail carries the lowercase one, and nothing is migrated. + ## Primary-thread root resolution `readHubSnapshot`'s caller resolves a workbench's primary-thread root from