Repository navigation
Hub: persist agent mail into participant mailboxes (CL-7449) - #638
Merged
Merged
Conversation
TheGreatAxios
force-pushed
the
cl-7449-mailbox-persist-wrap
branch
from
September 5, 2026 02:33
509f00f to
ec7af82
Compare
DB-gated coverage for the composition: an outbound frame addressed to two humans and one agent lands exactly two principal_mail rows (one per human), an SSE event per mailbox, and the upstream session_mail write still runs. Plus unit coverage for authorizeSender resolving a live sender run and refusing a dead one.
Every outbound agent frame reached the hub as a mail.outbound sidecar
frame and was persisted through the lookups' persistMail hook, which
writes session_mail only -- agent replies never landed in anyone's
mailbox. Wire createMailboxPersist (@corbits/mailbox) around
baseLookups.persistMail before the composed lookups reach the sidecar
handler, so every addressed human participant also gets a durable
principal_mail row, dual-written alongside session_mail in one
transaction with an SSE event on the mailbox bus.
authorizeSender (apps/hub/src/mailbox-persist.ts) resolves the sender
run address through the same resolveRoutableAddress the lookups
already use, then looks up that run's tenant's mail domain -- a
non-live sender resolves to null, which skips the mailbox write while
the frame still goes upstream. onRow stamps refs: [{ kind:
"workbench", id: tenantId }] onto each inserted row so a thread read
can scope by workbench; it owns its own try/catch and reports through
reportError; the persist call itself carries no second try/catch,
since dual-write independence is createMailboxPersist's own contract.
…stamp tenant.domain is globally unique, so three tests sharing one constant domain collided in CI's shared database. Mint a fresh domain per setup() call instead. Separately, createHubMailboxRowRefsStamper's onRow fires its DB update without the package awaiting it (onRow returns void by contract), so the refs assertion raced the update; poll briefly for it to land instead of reading once immediately after persistMail resolves.
…ery dedupe (CL-7449) Covers the mailbox-persist wrapper's critique findings: resolveRefs resolving the sender run's actual workbench (not the bench tenant), a bus subscriber seeing the ref at event time with no polling, the dual-write-independence branch (sessionless sender throws upstream, mailbox rows still land, zero session_mail rows), and same-frame redelivery dedupe. Also asserts the upstream session_mail row durably, not just via the returned value.
…saction (CL-7449) Fixes two critique findings on the CL-7449 wrapper: - The onRow hook stamped `senderAuthorization.tenantId` (the bench the run launched in) as the workbench ref, which is wrong -- a bench hosts many workbenches, and the run's own workbench is a separate id `@corbits/chat`'s `ChatStore.findWorkbenchByParticipantAddress` already tracks. `createHubMailboxResolveRefs` resolves it correctly, replacing `createHubMailboxRowRefsStamper`. - That resolution now runs as `createMailboxPersist`'s `resolveRefs` seam, inside the same transaction as the row insert, instead of an out-of-band `UPDATE principal_mail` issued after the fact -- so a bus subscriber's SSE event always carries the ref already, with nothing to poll for. Also documents (rather than silently accepting) the one remaining `resolveRoutableAddress` double-resolve: `authorizeSender` and the vendor-owned upstream `persistMail` each resolve the sender address once, and there is no seam to share that result without changing the vendor signature.
Covers resolveWorkbenchIdForAgentFrame's header-first resolution (an In-Reply-To/References hit names the parent row's own workbench even when the sender participates in several), the participant-scan fallback for header-less frames, and the ambiguous/absent cases (zero or several participations) stamping no ref and reporting mailbox_ref_unresolved once.
…7449) createHubMailboxResolveRefs resolved the workbench a reply's mailbox rows get stamped with by scanning the sender's workbench participation and taking the first match -- wrong for an agent in more than one workbench, since "first" is an arbitrary pick with no relation to which workbench the reply actually belongs to. @corbits/chat now exports resolveWorkbenchIdForAgentFrame: it reads the frame's own In-Reply-To (or, failing that, the newest References entry) and maps that Message-ID back to the timeline row it answers via findByMailMessageId -- that row's own workbenchId is authoritative. Only a header-less frame falls back to the participant scan, and only when ChatStore's new findWorkbenchIdsByParticipantAddress names exactly one workbench; zero or several matches stamp no ref and report mailbox_ref_unresolved instead of guessing. apps/hub/src/mailbox-persist.ts hands the helper both stores it needs (chatStore, roomMessages) plus the frame's decoded headers; index.ts hoists roomMessages next to chatStore so both are ready before the mailbox-persist wiring is built.
TheGreatAxios
force-pushed
the
cl-7449-mailbox-persist-wrap
branch
from
September 5, 2026 04:09
ec7af82 to
a1ac049
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
baseLookups.persistMailwith@corbits/mailbox'screateMailboxPersistbefore the composed lookups reach the sidecar handler, so every outboundmail.outboundframe also lands a durableprincipal_mailrow for each addressed human participant, dual-written alongside the existingsession_mailwrite.authorizeSender(apps/hub/src/mailbox-persist.ts) resolves the sender run address through the sameresolveRoutableAddresspersistMailalready uses, then reads that run's tenant's mail domain; a non-live sender resolves tonull, skipping the mailbox write while the frame still goes upstream. This is a genuine double resolve against upstream's own internal resolution of the same address — documented in the source as an accepted cost, since sharing one resolution would require changing the vendor-ownedpersistMailsignature.resolveRefs(replacing the earlieronRowstamper) resolvesrefs: [{ kind: "workbench", id: workbenchId }]insidecreateMailboxPersist's own transaction, so a bus subscriber's SSE event always carries the ref already, nothing to poll for. It is now header-first (CL-7449 follow-up):@corbits/chat's newresolveWorkbenchIdForAgentFramereads the frame's ownIn-Reply-To(or, failing that, the newestReferencesentry) and maps that Message-ID back to the timeline row it answers viafindByMailMessageId— that row's ownworkbenchIdis authoritative, since an agent participating in several workbenches at once has no single honest answer from its address alone. Only a header-less frame falls back to the participant scan (ChatStore.findWorkbenchIdsByParticipantAddress), and only when that scan names exactly one workbench; zero or several matches stamp no ref and reportmailbox_ref_unresolved(sender/tenant context) instead of guessing.workbenchIdis resolved from the sender RUN's own workbench, notsenderAuthorization.tenantId: an agent run launches in its parent BENCH tenant (whatauthorizeSenderresolves, and what the mailbox rows themselves are scoped under), and a bench hosts many workbenches — conflating the two would stamp every row with an id no workbench thread read can ever resolve.Fixes CL-7449
Part of CL-7445
Test plan
bun run scripts/typecheck.tsbun run lint(prettier + eslint)bun run check:structuralbun test packages/chat/src/resolve-workbench-ref.test.ts— 4/4 pass: header resolves to the parent row's workbench even with two participations; no-header + single participation resolves; no-header + two participations stamps nothing and reportsmailbox_ref_unresolved; no-header + zero participations likewise.bun test apps/hub/test/mailbox-persist.test.tsagainst real Postgres — 7/7 pass, including the new header-first-with-two-workbenches case, dual-write-independence (sessionless sender), redelivery-dedupe, and no-workbench-participation cases, plus asserting the workbench ref is visible to a bus subscriber at event-fire time with no polling.bun test apps/hub/test/composition.test.ts— 11/11 pass, boots the real hub with this wiring.bun test(apps/hub) — 279/279 pass.bun test(packages/chat) — 932/932 pass.