Repository navigation
Commit 7739573
fix(core, service-messaging): a failed settings read is retried, not dead-lettered as TEMPLATE_INVALID (#27730)
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F
<!-- claude-projects-attribution: {"github_login":"os-sales"} -->
_Requested by **Sales** · [project
thread](https://claude.ai/code/project/chan_01D5bWFY3rFzgJcytD5gD4Uu?thread=cmsg_01D5bWFY3rFzgJcytD5gD4UuPMZ8BHWtPf6nBTp51qSM64)_
Fixes #27257
Clause-②: yes (widening) — `resolveLocalizationContext`'s result
(`@objectstack/core`) gains one optional member, `readFailed?: true`.
Nothing is narrowed.
Metadata-protocol: none
## 维护者速读
- 改了什么:`resolveLocalizationContext` 的结果新增可选字段 `readFailed: true`,只在直连
`sys_setting` 读取抛错时出现(新读与 30
秒失败记忆命中都带)。消息服务的邮件、站内信渠道据此区分:读失败时不带币种照常渲染;若被拒(`TEMPLATE_INVALID`),再以
ISO「无币种」码 `XXX` 作默认试渲染一次(只渲染、不发送):能过,说明唯一的拒绝是缺默认,改记为可重试;仍被拒(如 `@path`
值错误),原样返回那句拒绝、判永久。读成功且确实没有默认币种时,仍是 `TEMPLATE_INVALID`、永久失败、带补救说明。
- 为什么改:原先读失败与"没有默认币种"都返回 `currency: undefined`,一次瞬时的设置读取故障会让带无参
`currency` 洞的邮件进死信,永不发送。分诊 6496449802:读失败是瞬时的,要重试;只有读成功且无默认才是永久的。
- 风险与回滚:扩大一个已发布导出类型(可选字段,`Clause-②: yes (widening)`),7
个调用方源码兼容、类型检查通过;失败记忆不加长,#11877 不变。不含无参 currency 洞的模板在读故障期间照常发送。回滚即
revert。
- 先看:`packages/core/src/security/resolve-authz-context.ts`(结果类型与
`resolveLocalizationContext` 中 `value`
的构造及失败记忆写入);`packages/services/service-messaging/src/email-channel.ts` 的
`templateRefusalAfterCurrencyReadFault`;`messaging-service-plugin.ts` 的
`getTenantCurrency`。
- 两分钟验证:`pnpm --filter @objectstack/service-messaging exec vitest run
src/template-tenant-currency.test.ts`,预期 21 条全过(读失败两渠道判
retryable、记忆命中同样、读成功无默认判 permanent、读失败下与币种无关的拒绝判 permanent)。
- 需要你做的:在本 PR 上批准这次公开类型扩面(分诊 6506866652 所要求);契约复核与增量复核均已
PASS(6508920767、6509670944),批准后席位落地。
## What changes
**The resolver says which of the two it is.**
`resolveLocalizationContext` already computed `backendFailed` (the
direct `sys_setting` read threw) but returned only the value, and the
failure memo returned the bare value on a hit. The answer now carries
`readFailed: true` in that case. It is built once and the memo stores
that same object, so a hit inside the 30 s failure TTL says the read
failed too. A settings-service refusal still does not count as a backend
fault (#11877 unchanged), and the memo's lifetime is unchanged. On every
successful read the member is absent.
**Messaging grades from it.** `getTenantCurrency`
(`messaging-service-plugin.ts`) keeps its signature and rejects when the
read failed; with no default after a successful read it answers
`undefined` as before. The email and inbox channels catch the rejection,
render without a currency, and pass a render failure through
`templateRefusalAfterCurrencyReadFault` (`email-channel.ts`, shared by
both channels, not exported from the package entry): a
`TEMPLATE_INVALID` that followed a failed read triggers ONE trial render
(render only, never sent) with the ISO no-currency code `XXX` as the
default. If that render passes, the missing default was the only
refusal, and the failure is recorded as `the template was not rendered:
the organization's default currency could not be read (...); the
delivery is retried until that read succeeds`, which carries no
`TEMPLATE_*` code, so `classifyError` grades it `retryable`. If the
trial render still answers `TEMPLATE_INVALID` (for example an `@path`
value refusal a default cannot cure), that refusal is returned as is and
stays `permanent`. An email service with no `renderTemplate` cannot tell
the two apart and is graded retryable (the helper's docblock says so).
Every other failure and every refusal after a successful read is
returned unchanged.
**Public surface.** One optional member on the result type of an
exported function of `@objectstack/core` (reachable from the package
root through `security/index`). No new export name, no input change.
`EmailChannelOptions` / `InboxChannelOptions` are unchanged in type;
their `getTenantCurrency` JSDoc now states the rejection.
## Tests
- `packages/core/src/security/resolve-localization-read-failed.test.ts`
(new): a throwing direct read answers the built-ins with `readFailed:
true`; the memo's hit keeps it with no second read; a successful read
with no currency has no `readFailed`; a settings refusal in front of a
successful direct read has none (#11877); past the TTL a recovered read
answers the stored default with no kind (the memo is not lengthened).
- `resolve-authz-context.test.ts` and
`resolve-localization-cache.test.ts`: two existing failure-memo pins
that compared the whole answer with `toEqual` now include `readFailed:
true` (moved pins, same behaviour otherwise).
-
`packages/services/service-messaging/src/template-tenant-currency.test.ts`:
through the plugin's own wiring (`MessagingServicePlugin.init`,
`kernel:ready`, `getChannel`), for both the email and inbox channels: a
failed read renders without `currency` and the refusal is `retryable`
with no `TEMPLATE_` text; a second delivery served from the failure memo
is `retryable` again with no second read; a successful read with no
default gives exactly the render's `TEMPLATE_INVALID` sentence (with its
remedy), graded `permanent`; a template that needs no default is
delivered during a read fault; `TEMPLATE_NOT_FOUND` after a failed read
stays `permanent`; a read that finds `CNY` formats in it; under a failed
read, an `@path` refusal stays `permanent` as is, and a template with
both a missing default and an `@path` refusal returns the `@path`
refusal, `permanent`, nothing sent (2 pins per channel); the retryable
pins assert the trial render used `XXX` and sent nothing.
## Acceptance notes
- **Branch name.** The card thread's harness-pinned branch
`claude/project-thread-tfwfer` names no issue; claim 6507228661 is the
identity record.
- **Same family, not here.** `plugin-auth` (`withMailTenantCurrency`
wiring) and `service-automation` (`setTenantCurrencyResolver` wiring)
also read a failed read as no default; triage filed them as #27710,
`Blocked-by: #27257`, which reads the new member.
- **A persistent fault.** Where the direct read fails for a long time (a
`sys_setting` table that does not exist yet), a delivery whose template
has a no-argument `currency` hole is now retried along the retry ladder
before it dead-letters, instead of dead-lettering at once. A template
without such a hole is unaffected.
- **The PR body's attribution lines** name this card thread (the harness
requires its own thread link), not the seat thread `order-common.md`
names.
## Verification
**Final head `4397f06d`** (S1 fix of contract review 6508920767 at
`56a2a5cf`; `4397f06d` changes only the changeset prose). Seat-relayed
readings from the card thread at `56a2a5cf`: ablations A4 (never
re-grade) 6 red, A5 (re-grade every refusal) 3 red, A6 (ignore the trial
render's answer) 4 red, each restored blob == HEAD;
`@objectstack/service-messaging` 62 files / 755 tests passed, typecheck
exit 0, eslint 0; derived gates 66/66 exit 0, `--ran` 0 NOT-MEASURED. CI
on `4397f06d`: all required checks green. Delta contract review
6509670944: PASS.
First head `aef05cf6` (merges `origin/main` at `ecd944f5`). The full
runs below were taken on `55cc3270`, before the merge; after it, the
scoped re-checks of the last bullet.
- **Ablations** (`scripts/ablation-replace.mjs`, wrap mode, on
`55cc3270`; each leg red, then restored with blob == HEAD and `git diff
HEAD` empty):
- A1, no kind on the fresh answer (`value = read`): core 3 of 5 red,
messaging 4 of 17 red.
- A2, the memo stores the bare value (`value: read` in the failure
entry): core 1 red (the memo-hit pin), messaging 2 red (the memo-hit
pins).
- A3, the plugin never rejects: messaging 4 red.
- A4, the channel never re-grades: messaging 4 red.
- A5, the channel re-grades every `TEMPLATE_INVALID`: messaging 3 red
(both "no default stays permanent" pins and the existing permanent pin).
- The first A4 attempt was refused by the tool (its replacement text
already occurred in the file) and restored; it was rerun with a distinct
replacement.
- **Tests and types** (under `os-verify-lock`, on `55cc3270`):
`@objectstack/core` test 127 files / 2853 tests passed;
`@objectstack/service-messaging` test 62 / 751 passed; typecheck exit 0
for `core`, `service-messaging` and the callers `mcp`, `plugin-audit`,
`plugin-auth`, `plugin-hono-server`, `service-automation`, `rest`.
- **Gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 66 families; each was run with its
exit code recorded. `--ran`: 66 derived, 66 run, 0 NOT-MEASURED, 0
UNRUN, all exit 0. The first run of `check:objectql-double-limit`
flagged both new `sys_setting` doubles as unjudged; they now hold the
caller's bound, and the gate is green. `check:i18n` and
`check:dual-build-cjs-loads` needed the full `turbo run build` (114
tasks) first.
- **Lint:** `pnpm lint` exit 0; ESLint on the changed files, 0 errors.
- **After the merge** (`aef05cf6`): rebuilt `@objectstack/core...` and
`service-messaging`; typecheck exit 0 for both; `@objectstack/core`
`src/security` 60 files / 1164 tests and
`@objectstack/service-messaging` 62 / 751 passed.
- **Open PRs:** none of the 26 open PRs touches these files.
- **Dogfood battery:** not run, because the title has no `!` and there
is no `(narrowing)` arm.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L6bhgUg7mjioboFdpaj1fQ)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01L6bhgUg7mjioboFdpaj1fQ
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent b00e3f4 commit 7739573
9 files changed
Lines changed: 431 additions & 18 deletions
File tree
- .changeset
- packages
- core/src/security
- services/service-messaging/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
455 | 455 | | |
456 | 456 | | |
457 | 457 | | |
458 | | - | |
| 458 | + | |
| 459 | + | |
459 | 460 | | |
460 | 461 | | |
461 | 462 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2211 | 2211 | | |
2212 | 2212 | | |
2213 | 2213 | | |
2214 | | - | |
| 2214 | + | |
| 2215 | + | |
| 2216 | + | |
| 2217 | + | |
| 2218 | + | |
| 2219 | + | |
| 2220 | + | |
| 2221 | + | |
| 2222 | + | |
| 2223 | + | |
| 2224 | + | |
| 2225 | + | |
| 2226 | + | |
| 2227 | + | |
| 2228 | + | |
| 2229 | + | |
2215 | 2230 | | |
2216 | 2231 | | |
2217 | 2232 | | |
| |||
2630 | 2645 | | |
2631 | 2646 | | |
2632 | 2647 | | |
2633 | | - | |
| 2648 | + | |
| 2649 | + | |
| 2650 | + | |
2634 | 2651 | | |
2635 | 2652 | | |
2636 | 2653 | | |
| |||
2667 | 2684 | | |
2668 | 2685 | | |
2669 | 2686 | | |
2670 | | - | |
| 2687 | + | |
| 2688 | + | |
| 2689 | + | |
| 2690 | + | |
2671 | 2691 | | |
2672 | 2692 | | |
2673 | 2693 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
463 | 463 | | |
464 | 464 | | |
465 | 465 | | |
| 466 | + | |
466 | 467 | | |
467 | 468 | | |
468 | 469 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
117 | 117 | | |
118 | 118 | | |
119 | 119 | | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
120 | 123 | | |
121 | 124 | | |
122 | 125 | | |
123 | 126 | | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
124 | 172 | | |
125 | 173 | | |
126 | 174 | | |
| |||
351 | 399 | | |
352 | 400 | | |
353 | 401 | | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
354 | 411 | | |
355 | | - | |
356 | 412 | | |
357 | 413 | | |
358 | 414 | | |
| |||
377 | 433 | | |
378 | 434 | | |
379 | 435 | | |
380 | | - | |
381 | | - | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
382 | 451 | | |
383 | 452 | | |
384 | 453 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| |||
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
64 | 67 | | |
65 | 68 | | |
66 | 69 | | |
| |||
194 | 197 | | |
195 | 198 | | |
196 | 199 | | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
197 | 209 | | |
198 | | - | |
199 | 210 | | |
200 | 211 | | |
201 | 212 | | |
| |||
209 | 220 | | |
210 | 221 | | |
211 | 222 | | |
212 | | - | |
213 | | - | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
214 | 237 | | |
215 | 238 | | |
216 | 239 | | |
| |||
0 commit comments