Repository navigation
fix(service-messaging): a notification delivery's title names the person it is for, not their raw user id - #25263
Conversation
Claude-Session: https://claude.ai/code/session_01WNppskqdULWrMoXfU9sgo1 Co-authored-by: Claude <noreply@anthropic.com>
…angeset Claude-Session: https://claude.ai/code/session_01WNppskqdULWrMoXfU9sgo1 Co-authored-by: Claude <noreply@anthropic.com>
…lated Claude-Session: https://claude.ai/code/session_01WNppskqdULWrMoXfU9sgo1 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 2 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 5 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1741e688cdc1f0908fbc61039e98109b7af3c977 && git checkout 1741e688cdc1f0908fbc61039e98109b7af3c977
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0b8bce40e081477132d81b1e11a19cd3cdb331c0 009bbc021ed8e031aa5567f2ed4408b0c10dd958 && git checkout -B drift-repro 0b8bce40e081477132d81b1e11a19cd3cdb331c0 && git merge --no-ff 009bbc021ed8e031aa5567f2ed4408b0c10dd958
node scripts/docs-audit/affected-docs.mjs --json 0b8bce40e081477132d81b1e11a19cd3cdb331c0
|
Contract reviewServed-tier: Face: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 · ACCEPT · 2026-10-22T17:41Z
Generated by Claude Code |
Requested by Sales · project thread
Fixes #25194
Clause-②: yes (widening)
Metadata-protocol: none
A notification delivery's record title (Setup → Notification Deliveries) read
channel → recipient_id, so a delivery to a user showed the raw user id. It now names the person, the shape PR #25178 gave the preference title: the user's name, else their email. A delivery to an address no user holds keeps the address as its title.The card's first measurement:
recipient_idstays textrecipient_idis an address, not only a user id, by design:RecipientResolver.resolveEmailkeeps an email no user holds verbatim, and a bare audience spec that is not a user id passes throughresolveOneunchanged.email-channel.ts,resolveRecipient, theEMAIL_SHAPEbranch), and the SMS channel texts a phone-shaped one directly (sms-channel.ts, thePHONE_SHAPEbranch).The outbox writes under
OUTBOX_SYSTEM_CONTEXT(isSystem: true), which skips the engine's reference check. Asys_userlookup there would store every such address as a dangling reference. So the person gets their own column.What changes
All in
@objectstack/service-messaging:objects/notification-delivery.object.tsrecipient_user_id:Field.lookup('sys_user', { required: false }). It has nomaxLengthand keeps the lookup defaultset_null, because the delivery is history and outlives the user.display_titlereads the person one hop through it:record.channel + ' → ' + (isBlank(trim(record.recipient_user_id.?name.orValue(''))) ? record.recipient_user_id.?email.orValue(record.recipient_id) : record.recipient_user_id.?name.orValue('')).recipient_user_idis read only through the hop andrecipient_idonly bare, so the formula has notraversal-bare-and-traversedconflict.recipient_idkeeps its type, width, requiredness and dedup-key role. Its label is now Recipient (it was "Recipient User"), and it gains a help text saying it holds an address.titleFormatis unchanged, as on the preference object.messaging-service.ts:enqueueDeliveriesmakes onesys_userread per emit (find,id $inthe distinct recipients,fields: ['id'], underFAN_OUT_SYSTEM_CONTEXT). It passesrecipientUserIdonly for an id that read found. If the read fails, the emit logs onewarnand stamps nothing; it never fails. The column is display-only, so this is a functional degradation, not a durability one.outbox.ts/sql-outbox.ts:EnqueueDeliveryInput.recipientUserId?is written torecipient_user_id. The memory outbox has no such column and ignores it. No claim, ack or send path reads it.fan-out-system-context.ts: the docblock that lists every read under the opt-in now names the new read.enis regenerated. Inzh-CN,recipient_id,recipient_user_idand the title help are retranslated and their fill hashes dropped. Ines-ESandja-JP, this object is untranslated English fill throughout. There, the two leaves whose source moved were refilled from the current source with fresh hashes, so no superseded draft is served.Existing rows have no
recipient_user_id, so they keep their old title until the 90-day window ages them out. Nothing is migrated.Tests
src/delivery-recipient-user.integration.test.ts(new, 7 tests): a realObjectQLoverSqlDriver(better-sqlite3) with the realsys_user, driven throughMessagingService.emitandSqlNotificationOutbox. It pins:sys_userread: 2 rows still enqueued, titles fall back, onewarn;channelandrecipient_idrefused when missing, and no storeddisplay_titlecolumn.src/objects/notification-display-title.test.ts: the delivery case moves out (its'email → usr_alice'title is the one reversed above), as the preference case did in PR fix(service-messaging,metadata-protocol)!: a notification preference's User is a sys_user reference, so its title names the person and the default for every user is an empty User instead of '*' #25178.recipientUserId: users.has(recipient) ? recipient : undefined,withrecipientUserId: undefined,(viascripts/ablation-replace.mjs, restored by the tool and checked withgit diff HEADempty) turns 3 of the 7 new tests red: the stamp, the titles and the delete fallback.pnpm --filter @objectstack/service-messaging test: 57 files, 629 tests pass.typecheckpasses. ESLint over the changed files is clean.dist/:service-automation:notify-organization-stamp,notify-delivery-outcomeandnotify-zero-delivery-visibility(24 tests) pass;plugin-approvals:reminder-action-link-at-rest(4 tests) passes.Gates
Run on head
009bbc02, branched fromorigin/main6bc8cc6a7. The three commitsorigin/mainhas gained since then touch no file inservice-messaging.node scripts/pm/dispatch-gates.mjs --commandsderived 69 commands. 68 exit 0, and--ranreconciliation reads 69 accounted, 68 run, 1 NOT MEASURED.check-plugin-teardown-shape --self-testfirst exited 3 because the shallow clone lacked its pinned fixture commit. It exited 0 aftergit fetch --depth=1of that commit.pnpm check:dual-build-cjs-loadsis NOT MEASURED (exit 3, PREREQUISITE NOT MET): it reads every package'sdist/, and this container built only the closures named here. This PR's CI run covers it.pnpm check:i18n(--filter=messaging, in sync) andpnpm check:i18n-stale-fill(0 stale fills): exit 0.pnpm check:i18n-coverageis NOT MEASURED locally (exit 3):examples/app-showcaseneeds unbuilt connectordist/. It is not in the derived set; CI covers it.dogfood_battery: n/a. The title has no!, andClause-②has no(narrowing)arm.Acceptance notes
claude/project-thread-m0hnco. It names no issue, so the Duplicate Fix Guard's branch-name hint will not match; the claim comment on service-messaging: sys_notification_delivery.display_title names the recipient by raw id (channel → recipient_id), the shape #24895 retired for preferences (ruling A item 5) #25194 is the identity record.sys_inbox_message.user_idandsys_notification_receipt.user_idare also text columns labelled "Recipient User". Neither is a record title (the receipt's title isstate; the inbox message's istitle), so they are not this card's defect. Noted, not filed.Generated by Claude Code