Skip to content

fix(spec,service-automation)!: errorHandling.maxRetries has one default, and strategy: 'retry' states its count (#4247) - #4266

Merged
os-zhuang merged 1 commit into
mainfrom
claude/max-retries-default-conflict-136cvv
Jul 31, 2026
Merged

fix(spec,service-automation)!: errorHandling.maxRetries has one default, and strategy: 'retry' states its count (#4247)#4266
os-zhuang merged 1 commit into
mainfrom
claude/max-retries-default-conflict-136cvv

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Closes #4247.

先说一个把结论改了的发现

issue 把两条路径列成并列的两种可能。实际读了调用图之后,其中一条不存在

retryExecution 只有一个调用点(execute()flow.errorHandling?.strategy === 'retry'),flow 来自 this.flows;而 this.flows 只有两处写入 —— registerFlowFlowSchema.parse 之后)和版本回滚(重新放回一份已经 parse 过的快照)。没有绕过 schema 喂给引擎的路,所以 ?? 3 在今天的调用图里根本够不到。

这不是"少了一件事要修",而是把问题换了一个:真正在跑的行为是唯一那条 —— strategy: 'retry' 不写次数 ⇒ maxRetries.default(0) 填成 0 ⇒ 一次都不重试。也就是 issue 末尾那个"顺带"其实是主症状:声明了 retry,运行时不 retry,正是 Prime Directive #10 推论说的"不要展示运行时并不交付的能力"。

?? 3 仍然要删 —— 它是第二份契约,随时可能因为新增一条载入路径而复活成真 bug。)

改了什么

1. 引擎不再自带默认值(PD #12)。 retryExecution 的参数从"五个 optional 字段 + 五个 ??"改成 parse 后的 NonNullable<FlowParsed['errorHandling']>,直接解构。这样安全,是因为上面那条不变量(this.flows 只装 parse 输出);而用 parsed 类型做参数,也是防止第二套默认值长回来的机制 —— 哪天 spec 不再给某个旋钮默认值,这里是编译错误,不是又一次"引擎悄悄猜一个"。

2. strategy: 'retry' 必须写 maxRetries >= 1(BREAKING)。 引擎那份删掉之后,"不写"就明确是 0 了 —— 而重试 0 次就是 strategy: 'fail' 换个名字。这里没有替 owner 在 0 和 3 之间选,而是让"不写"这个状态不再合法:schema 用 superRefine 拒绝两种拼法(省略→默认 0,和显式写 0),错误信息带处方。

为什么是"拒绝"而不是"默认 3":flow 级重试是整条流程从头再跑一遍,已经成功的 CRUD 节点会再执行一次、外呼会再发一次。给只写了 strategy: 'retry' 的存量流程静默改成跑 3 遍,是一次无声的行为变更;拒绝则是一行就能修的响亮失败,且作者拿回的正是他本来以为自己声明的东西。maxRetries: 0'fail'/'continue' 下仍然合法(它们根本不读)。

3. 迁移链把它作为 semantic TODO 暴露。 加了 flow-retry-max-retries-required:这一项没有无损重写 —— 填 0 保住了 parsed 路径的实际行为却违背作者写下的字面意思,填任何正数都是一个新决定。所以 os migrate meta 报给你,而不是替你改。(这条是被 packages/cli/test/migrate-meta.e2e.test.ts 逼出来的:它断言迁移产物在当前 schema 下可解析,而 fixture 的 { strategy: 'retry', fallbackNodeId: 'n9' } 去掉 fallbackNodeId 之后正好撞上新约束 —— 一个很好的证据,说明这类元数据确实存在于升级路径上。)

改动清单

文件 改动
packages/spec/src/automation/flow.zod.ts superRefine 拒绝 'retry' + maxRetries < 1.describe 与注释写明"这里是唯一的默认值来源"
packages/services/service-automation/src/engine.ts 删掉 retryExecution 的五个 ??,参数改为 parsed 类型
packages/spec/src/migrations/registry.ts step17 新增 semantic TODO + rationale 段落
packages/spec/liveness/flow.json maxRetries 条目补 note/verifiedAt —— 双默认值这件事记进账本
packages/cli/test/migrate-meta.e2e.test.ts fixture 补 maxRetries: 2(它测的是 fallbackNodeId 移除,不是重试次数)
content/docs/automation/flows.mdx 补全每个旋钮的默认值;新增"strategy: 'retry' 得说重试几次"小节;讲清 maxRetries 数的是重跑次数不是总次数
content/docs/releases/v17.mdx Smaller breaking changes 一条,带 FROM → TO
docs/protocol-upgrade-guide.md, packages/spec/spec-changes.json 生成物,gen:upgrade-guide / gen:spec-changes 重跑

测试

新增 packages/services/service-automation/src/flow-retry-attempt-count.test.ts(6 例)—— 钉住的是从 schema 看不出来的那部分:次数本身maxRetries: 2 ⇒ 总共跑 3 次(既不是旧 parsed 路径的 1 次,也不是旧 ?? 3 的 4 次);两种非法拼法在 registerFlow 被拒;'fail' 下写满旋钮仍然只跑一次;以及"引擎拿到的 block 五个字段都是实值"这条让删 ?? 得以成立的不变量。

spec 侧 flow.test.ts 新增 #4247 describe(5 例),并修正一处原本用 { strategy: 'retry' } 当载体去测 backoff 默认值的用例。

  • @objectstack/spec:276 files / 7154 tests ✅
  • @objectstack/service-automation:44 files / 482 tests ✅
  • migrate-meta.e2e:6/6 ✅
  • 本地门禁:check:livenesscheck:docscheck:authorable-surfacecheck:spec-changescheck:upgrade-guidecheck:strictness-ledgercheck:skill-examplescheck:doc-authoringcheck:nul-bytescheck:release-notes 全绿;改动文件 eslint 干净

两条与本 PR 无关的环境噪音

  1. pnpm --filter @objectstack/spec check:generatedmain 上就是红的(stash 掉全部改动复验过):check:strictness-ledgerfeat(spec): 让 #4001 严格性账本接受机器校验(首次运行抓到 11 处漂移) #4232 新加的脚本,没有在 check-generated.tsGATED / NO_GENERATOR 里分类。这个 PR 的 CI 不会因此变红 —— Check Generated Artifacts job 跑的是 check:skill-docs / check:spec-changes / check:upgrade-guide / check:authorable-surfacecheck:generated 本身没有任何 workflow 调用。已由 check:generated 的双向对账从不在 CI 上执行——#4203 关闭次日即被 #4232 原样复现,main 的本地 wrapper 又红了 #4255(正是"从不在 CI 上执行"这一点)、check:generated fails on main: check:strictness-ledger (#4232) was never classified in its ledger #4265[P3] spec: check:generated fails its own ledger reconciliation — check:strictness-ledger is unclassified #4267 覆盖,不在本 PR 范围内。
  2. @objectstack/cli 有 8 个 e2e 用例失败serve-no-artifactserve-boot-diagnosticsemit-json-pipesqlite-occupancy):只在我这个容器里,干净树上一模一样地失败 —— 工作区没有全量构建导致的,CI 上不复现。

…ault, and `strategy: 'retry'` states its count (#4247)

`flow.errorHandling.maxRetries` was declared twice with different values:
`FlowSchema` said `.default(0)`, while the engine's `retryExecution` read
`errorHandling.maxRetries ?? 3`. `??` fires only on `undefined`, so the
winner was decided by the ROUTE a flow took into the engine, not by what
its author wrote — 0 retries for a flow parsed by the schema, 3 for a
definition built by hand and handed to the engine. The neighbouring
`retryDelayMs ?? 1000` / `backoffMultiplier ?? 1` agreed with their
`.default()`s; only `maxRetries` disagreed.

The engine now keeps no defaults of its own: `retryExecution` takes the
parsed `NonNullable<FlowParsed['errorHandling']>` and destructures all
five knobs, no `??`. That is safe because `AutomationEngine.flows` only
ever holds `FlowSchema.parse` output (`registerFlow` parses; the
version-history rollback re-seats an already-parsed snapshot), and the
parsed parameter type is what keeps a second set of defaults from growing
back — a knob the spec stops defaulting becomes a compile error rather
than a silent engine-side guess (Prime Directive #12).

BREAKING: `strategy: 'retry'` now requires `maxRetries` >= 1. With the
engine's copy gone an unstated count is unambiguously 0, and 'retry' with
0 attempts runs the flow once and stops — `strategy: 'fail'` under another
label, a declared capability the runtime does not deliver. Rather than
pick 0 or 3 for the author, the schema refuses the combination in both
spellings (omitted → defaulted 0, and an explicit 0) with the
prescription in the message; a retry re-runs the WHOLE flow, side effects
included, so the count is the author's to state. `maxRetries: 0` stays
legal under 'fail'/'continue', which never read it.

The migration chain surfaces this as the semantic TODO
`flow-retry-max-retries-required` — there is no lossless rewrite, so
`os migrate meta` delegates the choice instead of guessing it.

Closes #4247

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SNpSdWvbikA9PiwXzkAH4i
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 31, 2026 1:17am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/service-automation, @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/service-automation, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/service-automation, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/service-automation, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 31, 2026 01:39
@os-zhuang
os-zhuang merged commit a648e96 into main Jul 31, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/max-retries-default-conflict-136cvv branch July 31, 2026 01:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

flow errorHandling.maxRetries 有两个默认值:schema 声明 0、引擎回退 3 —— 重试次数取决于流程有没有过 schema

2 participants