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

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 只有两处写入 —— registerFlow(FlowSchema.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:liveness、check:docs、check:authorable-surface、check:spec-changes、check:upgrade-guide、check:strictness-ledger、check:skill-examples、check:doc-authoring、check:nul-bytes、check:release-notes 全绿;改动文件 eslint 干净

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

  1. pnpm --filter @objectstack/spec check:generated 在 main 上就是红的(stash 掉全部改动复验过):check:strictness-ledger 是 feat(spec): 让 #4001 严格性账本接受机器校验(首次运行抓到 11 处漂移) #4232 新加的脚本,没有在 check-generated.ts 的 GATED / NO_GENERATOR 里分类。这个 PR 的 CI 不会因此变红 —— Check Generated Artifacts job 跑的是 check:skill-docs / check:spec-changes / check:upgrade-guide / check:authorable-surface,check: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-artifact、serve-boot-diagnostics、emit-json-pipe、sqlite-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.

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