feat(spec)!: converge RetryPolicy onto one declaration (#4661, C8) - #4670
Merged
Conversation
`@objectstack/spec/automation` and `@objectstack/spec/system` both exported `RetryPolicySchema` / `RetryPolicy` resolving to DIFFERENT declarations, so the shape a consumer got depended only on the import path (the #4411 trap). They were never two concepts: the `try_catch` node's `retry` region and `job.retryPolicy` both compute `delay = base * multiplier^(retry-1)`, and both executors implemented that identical formula. One declaration now lives in `shared/retry-policy.zod.ts`, re-exported by both entries, carrying the union of what the two sides could express. Because the published def key is derived from the entry namespace, both `automation/RetryPolicy` and `system/RetryPolicy` survive with an identical key set — so the convergence costs exactly ONE authorable key instead of eight. Authorable surface: `automation/RetryPolicy:retryDelayMs` is the single casualty, TOMBSTONED (`retiredKey`) rather than deleted because neither owning schema is `.strict()` — a plain removal would have Zod swallow the authored number and silently fall back to the 1000ms default. Defaults are the half no gate can see: the authorable-surface ratchet compares key sets, and a default is not a key. `job.retryPolicy` defaulted `maxRetries: 3` / `backoffMultiplier: 2` where automation defaulted 0 / 1. The merged declaration takes 0 / 1 (retry replays side effects, so it is opt-in), and the `retry-policy-converged` conversion writes the pre-17 numbers explicitly into every existing `job.retryPolicy` that omitted them — deployed stacks keep their exact behaviour; only a newly authored omission changes meaning. Filed separately as #4666 (gates are blind to default/constraint changes). `job.retryPolicy` gains `maxRetryDelayMs` and `jitter`, both now enforced in `runWithPolicy` rather than merely declared (ADR-0049). Baseline: 22 -> 20. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M9uWvoEp9CoLzYjNExj9sL
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
#4664 (spec key retirements + doc.tags) landed on main and touched the same five files. Resolution: - authorable-surface.json / spec-changes.json / docs/protocol-upgrade-guide.md REGENERATED from source (gen:schema / gen:spec-changes / gen:upgrade-guide), never hand-merged — hand-editing the authorable surface is forbidden (#4650). The regenerated surface differs from main by exactly this branch's delta: `automation/RetryPolicy:backoffMs` added, `:retryDelayMs` relabelled [RETIRED], and system/RetryPolicy gaining jitter / maxRetryDelayMs / the tombstone. - conversions/registry.ts auto-merged; verified 40 entries, zero duplicate ids, every declared conversion grouped, both `mappingInertKeysRemoved` (#4664) and `retryPolicyConverged` (#4661) present in the major-17 block. - migrations/registry.ts hand-resolved: both sides appended a paragraph to step17's `rationale` and an entry to `conversionIds`. Kept both. #4664's paragraph ended the string literal, so the concatenation was repaired and this branch's opener reworded ("Finally" -> "The same window") to avoid two "Finally"s in one rationale. Re-verified after the merge that exactly ONE conversion clause still ends in `.retryDelayMs` and it is `retry-policy-converged` — #4664 added five retired leaves (extractQuery / errorPolicy / batchSize / includeAll / placement), none of which collide with this cluster under the #4659 leaf-name match. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M9uWvoEp9CoLzYjNExj9sL
…y convergence #4664 retires `mapping.errorPolicy` in the same 17.0.0 window and its values included 'retry', so a reader could plausibly assume it migrates into a `retryPolicy` block. It does not — different type, inert enum, and its own prescription points at the import REQUEST's options. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M9uWvoEp9CoLzYjNExj9sL
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 2, 2026 16:12
github-merge-queue
Bot
removed this pull request from the merge queue due to a conflict with the base branch
Aug 2, 2026
生成物冲突以三路集合合并解决(等价于重新生成),手写登记表以 base→ours 的 hunk 打到 main 版上,双方条目均保留: - dual-source-exports.baseline.json: 22 → 19(C8 去 RetryPolicy ×2, main 的 #4662 去 ActivationEventSchema ×1) - conversions/registry.ts: retryPolicyConverged 与 main 的 objectManagedBySystemToSystemData 同时注册 - migrations/registry.ts: job-retry-policy-constraints-tightened 保留 - protocol-upgrade-guide.md: 两侧表格行都保留 未跑本地全套门禁 —— 由 CI 的 check:generated 验证生成物确实等于 重新生成的结果,这是与维护者商定的快路径(main 上 spec PR 密度使 本地十几分钟的门禁跑完即过期)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M9uWvoEp9CoLzYjNExj9sL
os-zhuang
enabled auto-merge
August 2, 2026 16:27
上一个合并提交用集合合并解决 spec-changes.json 的冲突,但该文件是 对象数组而非字符串数组,集合合并退化成「取变了的那一侧」,丢掉了 main 侧 #3355 的 object-managed-by-system-to-system-data 条目, check:spec-changes 因此报 stale。 改为真的跑 gen:spec-changes(+ gen:upgrade-guide 确认无漂移)。 现三条条目齐备:retry-policy-converged、 object-managed-by-system-to-system-data、 job-retry-policy-constraints-tightened。 check:spec-changes / check:upgrade-guide / check:skill-docs 均绿。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M9uWvoEp9CoLzYjNExj9sL
This was referenced Aug 2, 2026
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.
Fixes #4661
按维护者在 #4661 的裁决实施:路线 C1(收敛为能力并集,
backoffMs作为唯一基础延迟拼法)、正典默认值maxRetries: 0/backoffMultiplier: 1、约束取max(10)/min(1)/min(0)。基线 22 → 20。1. 四仓 import 语句级扫描:两侧都活,无死侧
RetryPolicySchema/RetryPolicy的 importcloud/cloud-v1/objectuiRetryPolicySchema排除本仓后total_count: 0objectstack本仓零 importer 不等于有死侧(#4653 已立此规矩)。两侧都被执行器按属性名消费:
service-job/src/run-with-policy.ts读maxRetries/backoffMs/backoffMultiplier;liveness/job.json的retryPolicy判live(「all drive the exponential-backoff retry loop … Enforced since [P2] Aspirational-config disposition: reconcile-or-prune the still-dead props from the 2026-06 liveness audit (Theme / Translation / Job / Webhook) #3494」)。service-automation/src/builtin/try-catch-node.ts经parseNodeConfig按TryCatchConfigSchema解析,5 个 key 全部真的实现了(含jitter抖动与maxRetryDelayMs封顶)。objectui的JobPreview.tsx从未类型化的Record读 system 拼法 —— typecheck 抓不到它,改 system 拼法会让该预览静默变哑。这是选backoffMs的实证理由之一。2. 立单前提修正:automation 侧的可达机制与立单描述不同(结论更强)
立单说 automation 侧「经
retry:进FlowSchema」。静态 $ref 图上不成立:flow.zod.ts:163的config是z.record(z.string(), z.unknown())(开放 record),TryCatchConfigSchema在flow.zod.ts里一次都没被引用。C8 判 ✅ 可达是靠 system 侧成立的:
BUILTIN_METADATA_TYPE_SCHEMAS.job = JobSchema→JobSchema.retryPolicy。但实质结论更强:automation 侧经节点 config 缝被作者真写、被执行器真解析,而
TryCatchConfigSchema不是.strict()—— 掉一个 key 就是 Zod 静默吞掉作者写的值。所以 tombstone 是硬要求,已照做。3. 两侧形状 diff(automation 5 key / system 3 key)
./automation./systemretryDelayMs,min 0,default 1000backoffMs,positive,default 1000backoffMs,min 0,default 1000maxRetriesbackoffMultipliermaxRetryDelayMsjitterRetryPolicy类型z.inputz.inferz.input(新增RetryPolicyParsed承载z.infer)并集不可能无损:
retryDelayMs与backoffMs是同一概念的两个拼写(两侧退避公式与两个执行器实现完全一致),同时声明两者就是 alias 反模式。所以必然丢一个 key —— 这是本 PR 唯一的可作者化损失。4. 为什么是
backoffMs,以及为什么声明放在shared/拼法选
backoffMs的证据(不是偏好):liveness/job.json明写「retryPolicyhere is the ENFORCED spelling ({maxRetries, backoffMs, backoffMultiplier})」;conversions/registry.ts的 datasource 账本判定的 20 条死键至今无人处置:三个块整块无人读,其中 readOnly 让一个 shipped 示例的「只读副本」可写(ADR-0049 enforce-or-remove) #4583 注释:「hook.retryPolicyandjob.retryPolicyARE enforced … spell the delaybackoffMs」;contracts/JobRetryPolicy、run-with-policy.ts、objectuiJobPreview.tsx)全部读backoffMs⇒ 零消费方改动;initialDelayMs一族),不预支那个更大的问题。声明落在新文件
packages/spec/src/shared/retry-policy.zod.ts,且刻意不进shared/index.ts。 关键机制:发布的 def key 形如入口命名空间/名字,由哪个入口 barrel 再导出决定(build-schemas.ts遍历命名空间对象)。因此一份声明被./automation与./system同时再导出时,两个 def key 都存活且 key 集合相同 —— 这正是本簇只付 1 个 key 而不是 8 个的原因。不进shared/index.ts是为了避免多出第三个shared/RetryPolicydef 和 5 行纯噪音(retired-key.ts/strict-object.ts同样不进 barrel,有先例)。放shared/而非任一域:control-flow.zod.ts会拉进整个flow.zod节点图,不该为一个 5 字段策略把它塞进system/job.zod.ts。5. 消失的 key:1 个,tombstone + conversion 证据
authorable-surface.json相对main的完整 diff(由gen:schema重写,未手编):零 key vanish —— 唯一的删除行是
retryDelayMs改标为[RETIRED]。system/RetryPolicy:retryDelayMs [RETIRED]是新增的「生而退休」key(同一声明),检查 (b) 只对 live → retired 触发,故不受影响;它的效果是好的:从 flow 抄了 retry 块的 job 作者会拿到明确处方。检查 (b) 把 surface 按
/切开后做endsWith('.' + leaf),任何以.retryDelayMs结尾的无关 clause 都能蒙混过关。我枚举了ALL_CONVERSIONS的全部 clause,合并 #4664 之后重跑:全仓仅 1 条匹配,就是本 PR 的,且
flow.node.config.retry.retryDelayMs字面上就是automation/RetryPolicy:retryDelayMs所在的位置 —— try_catch 节点 config 里的 retry 区域。不是巧合匹配。#4664 新引入的五个退休 leaf 各自对上自己的 conversion,与本簇零交叉。6. 默认值:conversion 把「静默翻转」变成「零行为变化」
本 PR 最容易被下游误读的一点,changeset 里已显式写明。
正典默认值取 automation 现值(
maxRetries: 0/backoffMultiplier: 1)。单看这一步会让存量 job 静默停止重试,所以retry-policy-convergedconversion 把 pre-17 的数值显式写进每个省略了它们的存量job.retryPolicy:⇒ 已部署的栈行为完全不变;改变的只是新写的省略是什么意思(= 不重试)。理由采纳裁决:重试会重放已经发生的副作用(job handler 的写入与外呼、try 区域的副作用),隐式重放是测试最难捕获、生产代价最高的失败模式;同一读法在
flow-retry-max-retries-required(#4247)已有先例。两个 walker 都是先验证再用,不是假设:
mapCollection(stack, 'jobs', …)(jobs确认是 bundle 集合键,app-plugin.ts:799-802)与mapFlowNodes(递归进config.body.nodes)。fixture 覆盖三种 job(省略两者 / 都写了 / 无 retryPolicy 块)与两种 try_catch(旧拼写 / 已正典),expectedNotices: 3。约束收紧(
maxRetries上限 10、backoffMultiplier下限 1)无无损改写,故登记为semantic迁移条目job-retry-policy-constraints-tightened,而非伪造 conversion。乘数小于 1 描述的是逐次变短的延迟 —— 对失败的依赖越重试越快,与 backoff 相反。7. Sabotage 验证(#4642:pin 必须是运行时断言且必须证明会红)
回归 pin 在
packages/spec/src/shared/retry-policy.test.ts,用运行时模块命名空间断言(不是编译期条件类型 —— 该包 tsconfigexclude掉**/*.test.ts,vitest 未开 typecheck,编译期 pin 空转)。Sabotage A —— 让
system重新自带一份声明(即回到双源):Sabotage B —— 只把
maxRetries默认值从0改成3(一个字符)。这条同时是 #4666 的证据:即:默认值翻转对门禁完全不可见,唯一的补偿控制是这个手写 pin。已按裁决单独立案 #4666(unassigned,未修)。
Sabotage B 在合并 #4664 之后重跑,仍然红(见 §10)。两个 sabotage 均已还原,
git status干净。8. 也顺手修好的既有缺陷
config-schemas.test.ts的 try_catch 断言从不检查基础延迟这个 key,而control-flow-form-zod-ledger.test.ts只比对顶层 config key(try/catch/errorVariable/retry)—— 所以嵌套的 retry 子键改名两个 parity 测试都不会红。已补上backoffMs存在 +retryDelayMs不存在的断言,把这个缝堵上。严格性台账已同步:
automation/88 → 87 sites,control-flow.zod.ts6 → 5,并在 Note 里写明该 site 去了哪、以及该台账只 section 了ui//data//automation//security//studio/,shared/不在其覆盖范围(既有 scoping 选择,非本 PR 造成,但值得维护者知道)。文档侧顺带消掉了一个既有混淆:原先
references/automation/job.mdx与references/system/job.mdx两页都叫 Job、各自文档一个形状不同的RetryPolicy,现在是同一形状的retry-policy.mdx两页。9. 与 #4664 的语义边界
17.0.0 同窗退休的
mapping.errorPolicy的取值里有'retry',读者可能误以为要迁到retryPolicy。不是 —— 它是另一个类型上的惰性枚举,处方指向「import REQUEST 自己的 options」,与本簇零关系。已在 changeset 里单列一节说明,避免误读。10. 合并
main(#4664)与复验main走到5966c2abe(#4664,又一个 spec 退休 PR),与本分支在 5 个文件相撞。处置:authorable-surface.json/spec-changes.json/docs/protocol-upgrade-guide.md)全部重新生成,零手工合并 —— 手编可作者化面是明令禁止的(authorable-surface 的 tombstone 门禁可被手编基线绕过 —— 删掉基线行就删掉了证据(#4638 / #4643 已两次这样过绿) #4650)。重新生成后该文件相对main的 diff 恰好只有本分支的 6 行(见 §5)。conversions/registry.ts自动合并;脚本核验:40 条条目、0 个重复 id、每条声明都在CONVERSIONS_BY_MAJOR里、mappingInertKeysRemoved(feat(spec)!: 退役五个「lint 永远无法告警」的键,并接通 doc.tags (#4509) #4664)与retryPolicyConverged(spec 双源清账 C8:RetryPolicy / RetryPolicySchema(./automation ≠ ./system)—— 2 条 #4661)都在 major 17 组内。migrations/registry.ts手工解决:双方都往 step17 的rationale追加段落、往conversionIds追加一条。两边全保留;feat(spec)!: 退役五个「lint 永远无法告警」的键,并接通 doc.tags (#4509) #4664 的段落结束了字符串字面量导致拼接断裂,已修复,并把本分支的开头由「Finally」改为「The same window」,避免一个 rationale 里两个 Finally。合并后全套重跑(不是只跑 build):
pnpm build(spec,含BUILD_DTS)check:dual-source-exports4314 names across 16 entry points — 163 re-exported (single declaration), **20** accepted dual-source (baseline)check:generatedAll 8 generated artifacts are up to date.pnpm test(spec)292 files / **7310** tests passedcheck:livenesscheck:strictness-ledger67 file(s) across 5 triaged director(ies) — site counts match, section totals balancecheck:empty-statecheck:variant-docscheck:exported-any1886 types + 1638 schemascheck:skill-examples202 prose examples type-checkservice-jobtest5 files / 39 testsservice-automationtest53 files / 649 testsretry-policy-converged: before → after, emits 3 notice(s)MATCH COUNT: 1(见 §5)expected 3 to be +0)pnpm typecheckTasks: 122 successful, 122 total(0 条error TS,exit 0).changeset/pre.json仍是mode: pre/tag: rc,major 通道未关闭,本簇赶在changeset pre exit之前。⛔ 未触碰
content/docs/releases/。PR 保持 draft。