Repository navigation
fix(service-automation): a currency hole refused after a failed tenant-settings read is a runtime failure, not a guard refusal - #28110
Conversation
… tenant-settings read as a runtime failure The automation plugin's tenant-currency resolver rejects when resolveLocalizationContext answers readFailed. Run setup and a resumed segment note the failed read beside that run's context, and the node failure arm re-renders a refused text slot with a placeholder currency: when that renders, the refusal becomes a plain runtime error a fault edge routes, and a retry can succeed. A read that succeeded is unchanged. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4czcfiRnuFCuLdmegYt
📓 Docs Drift Check7 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 10 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 e2b3ed0d86c48703afb5aa29ce1e7718edf75b06 && git checkout e2b3ed0d86c48703afb5aa29ce1e7718edf75b06
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9cbe5079ef5b398544268e7540ffa26cbad008b4 e62036b8e18e48f2858bd91c39677443db4a04e0 && git checkout -B drift-repro 9cbe5079ef5b398544268e7540ffa26cbad008b4 && git merge --no-ff e62036b8e18e48f2858bd91c39677443db4a04e0
node scripts/docs-audit/affected-docs.mjs --json 9cbe5079ef5b398544268e7540ffa26cbad008b4 |
|
domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL 维护者速读
席位意见:ACCEPT。 席位核对Head-sha:
|
Requested by Elon · project thread
domain:server#4 · os-elon-musk · session_016WV4czcfiRnuFCuLdmegYt
Fixes #28056
Clause-②: no (a refusal that followed a failed read of the organization's default currency is now an ordinary runtime failure, which a
faultedge routes; every run that succeeded still succeeds, and a refusal after a read that succeeded stays the guard refusal it was)Metadata-protocol: none
维护者速读
{{ amount | currency }}缺租户默认币种时,若是因为读sys_setting失败(而不是组织真没设),现在判为普通运行时失败:fault 边能接住,errorHandling: retry读恢复后就成功;报错说“设置读不出来”,不再叫人去设币种。readFailed,运行准备又把拒绝和“没有”一样吞掉,于是一次瞬时读故障被当成 guard 拒绝(不可路由),原因还写错。messaging 通道在 [finding]resolveLocalizationContextcannot tell "no tenant default" from "the settings read failed", so a transientsys_settingfault dead-letters a mail with a{{ x | currency }}hole as a permanentTEMPLATE_INVALID#27257 已按同样规则分级。packages/spec与AutomationContext,读失败标记放在本包 WeakMap,不持久化。回滚即 revert。packages/services/service-automation/src/builtin/template.ts(gradeAfterTenantCurrencyReadFault)、src/dag-traversal.ts(executeNode 的 throw 分支)、src/run-resume.ts(两处 catch)、src/plugin.ts(readFailed时拒绝)。pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 src/builtin/notify-currency-read-fault.test.ts src/builtin/notify-currency-tenant-default.test.ts,预期全绿:读失败被 fault 边接住、retry 后发出 CN¥ 标题;真没设币种仍不路由。What changes
FlowTenantCurrencyResolvergives a rejection its own meaning: the read FAILED.undefinedstill means the read succeeded and found no default.plugin.ts. The automation plugin's tenant-currency resolver rejects whenresolveLocalizationContextanswersreadFailed([finding]resolveLocalizationContextcannot tell "no tenant default" from "the settings read failed", so a transientsys_settingfault dead-letters a mail with a{{ x | currency }}hole as a permanentTEMPLATE_INVALID#27257), as the messaging channels'getTenantCurrencydoes. It used to returnlocalization.currencyand dropreadFailed.run-resume.ts. Run setup (resolveRunContext) and a resumed segment's re-read (withTenantCurrency) still leaveAutomationContext.currencyunset on a rejection, and now note the failed read beside that context (noteTenantCurrencyReadFault). The resumed segment notes a copy, so the persisted context never carries one segment's fault into a later resume. Nothing is added toAutomationContext(packages/spec) or the persisted run.builtin/template.ts.gradeAfterTenantCurrencyReadFault(err, variables, context): aFlowTextTemplateErrorthrown for a context whose read failed is rendered once more with the placeholder currencyXXX(ISO 4217's "no currency"). If that renders, the only refusal was the missing default, and a plainErrorwithout the guard brand is returned, saying thelocalization.currencysetting could not be read. If it still refuses, the guard refusal is returned as it stands. Any other failure, and every refusal after a successful read, is returned unchanged. Same probe as service-messaging'stemplateRefusalAfterCurrencyReadFault([finding]resolveLocalizationContextcannot tell "no tenant default" from "the settings read failed", so a transientsys_settingfault dead-letters a mail with a{{ x | currency }}hole as a permanentTEMPLATE_INVALID#27257).dag-traversal.ts.executeNode's throw arm grades the thrown failure first, so the failure step,$errorand theisGuardRefusalrouting decision all read the graded failure. This is the engine's existing classification (Node-level error branches: let a flow handle a failed node instead of aborting the run #3863): a non-guard failure is routable by afaultedge;errorHandling.strategy: 'retry're-runs a failed run either way. No new code.engine.ts. TheFlowTenantCurrencyResolverdocblock states the rejection meaning.The seam is shared by every text slot a node renders, so
httpandscreenslots are graded the same way asnotify.Why
A transient
sys_settingread failure failed anotifyrun as a permanent guard refusal: a declaredfaultedge could not route it, and the error told the operator to set a default currency the organization may already have.Verification
Measured on
main5448585dbefore the change (a scratch test, not committed): anotifyflow with afaultedge, the resolver throwing:faultedge declaredfailed; the edge is not taken; error "...the organization has none set... Set the default currency in Setup → Localization"faultedge;$error.messagesays thelocalization.currencysetting could not be readerrorHandling: retry(maxRetries: 2)CNYWon CN¥1,234.50Pins,
src/builtin/notify-currency-read-fault.test.ts(3 red onmain, 10/10 green on this head):faultedge and$errornames the failed read; retry succeeds once the read does; a successful read with no default keeps the guard refusal (not routed); a successful read with a default is unchanged.faultedge, and the persisted context stays as it parked.sys_settingread throws, the refusal is routed; over one that finds no default, the guard refusal stands.The existing
notify-currency-tenant-default.test.tsrow "refuses the node by name when the read fails" still holds (nofaultedge there, so the run still fails, naming the hole andlocalization.currency).Ablations (each restored through
scripts/ablation-replace.mjs, blob == HEAD after):readFailed→ 1 red (plugin).Suites:
@objectstack/service-automationfull vitest 228 files / 2886 tests pass;pnpm --filter @objectstack/service-automation typecheck(incl.check:test-typecheck) passes.dispatch-gates --commandsderived 62 families at heade62036b8: all exit 0 (check:dual-build-cjs-loadsafter thedogfood^...+runtime^...dist build),--ranreconcile 62/62, 0 NOT MEASURED;pnpm lintexit 0.mainmerged at549203d0; nothing onmainsince touchesservice-automation.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: not touched.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4czcfiRnuFCuLdmegYt