Skip to content

Commit 60a93b3

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-4673-retire-data-field-changed
# Conflicts: # docs/protocol-upgrade-guide.md # packages/spec/src/migrations/registry.ts
2 parents f098fd4 + b8b3c64 commit 60a93b3

27 files changed

Lines changed: 795 additions & 97 deletions

.changeset/converge-activation-event-schema.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,20 @@ enum 取**两侧 v17 前词表的并集**,共 9 个值:
7070

7171
手工迁移步骤:按上表把每个字符串改写成 `{ type, pattern }`**漏改会在 parse 处响亮失败** —— `StudioPluginManifestSchema``strictObject`,字符串遇到对象 schema 直接抛错,不存在静默吞掉或强制转换。
7272

73+
## 不要与同窗口的 #4509 / #4664 退休项混淆
74+
75+
v17 同窗口的 #4664 退休了五个键。其中 **`app.contextSelectors[].placement`** 与本条变更**毫无关系**,但很容易被读成有关系 —— 那条退休说明里写着「`location` 曾是 `placement` 的别名」,而 Studio 插件的面板贡献点**恰好也有一个 `location`**:
76+
77+
| |#4664 退休的 | 本次变更**未动**|
78+
|:--|:--|:--|
79+
|| `ui/App.contextSelectors[].placement`(`location` 是它的别名) | `studio/PanelContribution.location` |
80+
| 语义 | app 的上下文选择器渲染在哪(`sidebar_header` / `topbar`) | Studio 插件的辅助面板停靠在哪(`bottom` / `right` / `modal`) |
81+
| 状态 | 已删除 | **原样保留**,仍是可作者化键 |
82+
83+
两者在不同 schema 上、取值域不同、互不相关。写 Studio 插件的作者**不需要**因为 #4664 去动 `contributes.panels[].location`
84+
85+
其余四个退休键(`mapping.extractQuery` / `mapping.errorPolicy` / `mapping.batchSize` / `app.contextSelectors[].includeAll`)与 `activationEvents` 无任何语义交叉;同窗口的 #4668(ADR-0119 D2 migration journal)亦然。
86+
7387
## 其它影响
7488

7589
- `@objectstack/spec/studio` 现在**额外导出** `ActivationEvent` 类型(此前只有 schema),与 `./kernel` 指向同一份声明。
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
**Retry policy converges onto one declaration** (#4661 — the #4535 C8 dual-source cluster).
6+
7+
`@objectstack/spec/automation` and `@objectstack/spec/system` both exported
8+
`RetryPolicySchema` / `RetryPolicy`, resolving to **different declarations** — so the
9+
shape you got depended only on which entry you imported (the #4411 trap). They were
10+
never two concepts: the `try_catch` node's `retry` region and `job.retryPolicy` both
11+
compute `delay = base * multiplier^(retry-1)`, and both executors implemented that
12+
identical formula. There is now one declaration, re-exported by both entries, carrying
13+
the union of what the two sides could express.
14+
15+
## FROM → TO
16+
17+
| | FROM `./automation` | FROM `./system` | TO (both entries) |
18+
|---|---|---|---|
19+
| base delay | `retryDelayMs`, min 0, default 1000 | `backoffMs`, positive, default 1000 | **`backoffMs`**, min 0, default 1000 |
20+
| `maxRetries` | 0–10, default **0** | ≥0 unbounded, default **3** | 0–**10**, default **0** |
21+
| `backoffMultiplier` |**1**, default **1** | positive, default **2** |**1**, default **1** |
22+
| `maxRetryDelayMs` | default 30000 | *(absent)* | default 30000 |
23+
| `jitter` | default false | *(absent)* | default false |
24+
| `RetryPolicy` type | `z.input` | `z.infer` | `z.input` (+ new `RetryPolicyParsed` for `z.infer`) |
25+
26+
## What you must change
27+
28+
**1. Rename `retryDelayMs``backoffMs`** in any `try_catch` node's `retry` block.
29+
The value (milliseconds before the first retry) is unchanged. The old spelling is
30+
**tombstoned**, not deleted — it rejects with the rename prescription instead of being
31+
silently swallowed, because neither owning schema is `.strict()`. Automated:
32+
33+
```
34+
os migrate meta --from 16
35+
```
36+
37+
**2. Nothing for existing jobs — but read this if you author new ones.** `maxRetries`
38+
now defaults to **0** and `backoffMultiplier` to **1**, where `job.retryPolicy`
39+
previously defaulted to 3 and 2. Left alone that would silently stop deployed jobs from
40+
retrying, so the `retry-policy-converged` conversion **writes the pre-17 numbers
41+
explicitly into every existing `job.retryPolicy`** that omitted them:
42+
43+
```jsonc
44+
// before // after `os migrate meta`
45+
{ "backoffMs": 5000 } { "backoffMs": 5000, "maxRetries": 3, "backoffMultiplier": 2 }
46+
```
47+
48+
Deployed stacks therefore keep their exact behaviour. What changes is what a **newly
49+
authored** omission means: declaring a retry block without `maxRetries` now means *no
50+
retry*. Retry is opt-in because a retry replays whatever the attempt already did — a job
51+
handler's writes and callouts, a `try` region's side effects — and an implicit replay is
52+
the failure mode hardest to catch in tests and most expensive in production. (The same
53+
reading is already recorded for flow-level retry in `flow-retry-max-retries-required`,
54+
#4247.)
55+
56+
> This defaults change is the part **no gate can see**: the authorable-surface ratchet
57+
> compares key sets, and a default is not a key. It is called out here because a
58+
> changeset is the only channel that carries it.
59+
60+
**3. Two bounds now apply to jobs that did not have them**`maxRetries` is capped at
61+
**10** and `backoffMultiplier` floored at **1**. Both fail loudly at parse time rather
62+
than being silently reinterpreted; neither has a lossless rewrite, so they are recorded
63+
as the `job-retry-policy-constraints-tightened` semantic migration note. A multiplier
64+
below 1 described a delay that *shrinks* on each attempt — retrying a failing dependency
65+
ever faster, the opposite of backoff.
66+
67+
**4. `import type { RetryPolicy } from '@objectstack/spec/system'` is now the input
68+
shape** (every key optional) rather than the post-parse shape. Use the new
69+
`RetryPolicyParsed` where you need defaults applied.
70+
71+
## Not related to `mapping.errorPolicy` (#4509 / #4664, same release)
72+
73+
17.0.0 also retires `mapping.errorPolicy`, whose values included `'retry'`. That is a
74+
different thing on a different type: an inert enum on the stored **mapping**, whose
75+
prescription is "error handling on the import path belongs to the import REQUEST's own
76+
options". It does **not** migrate to a `retryPolicy` block, and nothing in this change
77+
affects it.
78+
79+
## What you gain
80+
81+
`job.retryPolicy` accepts **`maxRetryDelayMs`** (ceiling on a single backoff delay) and
82+
**`jitter`** (randomize each delay into [50%, 100%]). Both are enforced by
83+
`runWithPolicy`, not merely declared — jitter is what stops a fleet of jobs that failed
84+
on one outage from retrying in lockstep.

content/docs/references/automation/control-flow.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ const result = FlowRegionSchema.parse(data);
149149
| **try** | `{ nodes: { id: string; type: string; label: string; config?: Record<string, any>; … }[]; edges?: { id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[] }` || Protected region |
150150
| **catch** | `{ nodes: { id: string; type: string; label: string; config?: Record<string, any>; … }[]; edges?: { id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[] }` | optional | Handler region run when the try region fails |
151151
| **errorVariable** | `string` | optional | Variable holding the caught error in the catch region |
152-
| **retry** | `{ maxRetries?: integer; retryDelayMs?: integer; backoffMultiplier?: number; maxRetryDelayMs?: integer; … }` | optional | Optional retry policy for the try region |
152+
| **retry** | `{ maxRetries?: integer; backoffMs?: integer; backoffMultiplier?: number; maxRetryDelayMs?: integer; … }` | optional | Optional retry policy for the try region |
153153

154154

155155
---

content/docs/references/automation/job.mdx

Lines changed: 0 additions & 34 deletions
This file was deleted.

content/docs/references/automation/meta.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717
"webhook",
1818
"---Approvals & Jobs---",
1919
"approval",
20-
"job",
2120
"---More---",
2221
"builtin-node-config",
2322
"events-core",
2423
"flow-function",
2524
"io-node-config",
25+
"retry-policy",
2626
"schemaless-node-config"
2727
]
2828
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
title: Retry Policy
3+
description: Retry Policy protocol schemas
4+
---
5+
6+
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
7+
8+
## TypeScript Usage
9+
10+
```typescript
11+
import { RetryPolicySchema } from '@objectstack/spec/automation';
12+
import type { RetryPolicy } from '@objectstack/spec/automation';
13+
14+
// Validate data
15+
const result = RetryPolicySchema.parse(data);
16+
```
17+
18+
---
19+
20+
## RetryPolicy
21+
22+
### Properties
23+
24+
| Property | Type | Required | Description |
25+
| :--- | :--- | :--- | :--- |
26+
| **maxRetries** | `integer` || Retry attempts after the initial one. 0 (the default) means no retry — state a count to opt in. |
27+
| **backoffMs** | `integer` || Base delay before the first retry (ms); subsequent delays multiply by backoffMultiplier |
28+
| **backoffMultiplier** | `number` || Exponential backoff multiplier; 1 (the default) keeps the delay flat |
29+
| **maxRetryDelayMs** | `integer` || Ceiling for a single backoff delay (ms) |
30+
| **jitter** | `boolean` || Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries |
31+
| **retryDelayMs** | `any` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661) — the retry policy now has one spelling for its base delay across `job.retryPolicy` and a `try_catch` node's `retry`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. `os migrate meta --from 16` rewrites it for you. |
32+
33+
34+
---
35+

content/docs/references/system/job.mdx

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ Schedule jobs using cron expressions
1616
## TypeScript Usage
1717

1818
```typescript
19-
import { CronScheduleSchema, IntervalScheduleSchema, JobSchema, JobExecutionSchema, JobExecutionStatus, OnceScheduleSchema, RetryPolicySchema, ScheduleSchema } from '@objectstack/spec/system';
20-
import type { CronSchedule, IntervalSchedule, Job, JobExecution, JobExecutionStatus, OnceSchedule, RetryPolicy, Schedule } from '@objectstack/spec/system';
19+
import { CronScheduleSchema, IntervalScheduleSchema, JobSchema, JobExecutionSchema, JobExecutionStatus, OnceScheduleSchema, ScheduleSchema } from '@objectstack/spec/system';
20+
import type { CronSchedule, IntervalSchedule, Job, JobExecution, JobExecutionStatus, OnceSchedule, Schedule } from '@objectstack/spec/system';
2121

2222
// Validate data
2323
const result = CronScheduleSchema.parse(data);
@@ -62,7 +62,7 @@ const result = CronScheduleSchema.parse(data);
6262
| **description** | `string` | optional | Job description / purpose |
6363
| **schedule** | `{ type: 'cron'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; timezone?: string } \| { type: 'interval'; intervalMs: integer } \| { type: 'once'; at: string }` || Job schedule configuration |
6464
| **handler** | `string` || Handler function name (must match a key in `defineStack({ functions })`) |
65-
| **retryPolicy** | `{ maxRetries?: integer; backoffMs?: integer; backoffMultiplier?: number }` | optional | Retry policy: failed runs (including timeouts) are retried with exponential backoff (delay = backoffMs * backoffMultiplier^(retry-1)) up to maxRetries retries after the initial attempt (#3494). Omit for the legacy single-attempt behavior. |
65+
| **retryPolicy** | `{ maxRetries?: integer; backoffMs?: integer; backoffMultiplier?: number; maxRetryDelayMs?: integer; … }` | optional | Retry policy: failed runs (including timeouts) are retried with exponential backoff (delay = min(backoffMs * backoffMultiplier^(retry-1), maxRetryDelayMs), optionally jittered) up to maxRetries retries after the initial attempt (#3494). Omit the block for a single attempt; declaring it without `maxRetries` also means no retry since 17.0.0 (#4661) — state a count to opt in. |
6666
| **timeout** | `integer` | optional | Per-attempt time limit in milliseconds; an over-limit run is recorded with execution status "timeout" (#3494). The in-flight handler is abandoned, not forcibly cancelled. Omit for no time limit. |
6767
| **enabled** | `boolean` | optional | Whether the job is enabled |
6868
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
@@ -114,19 +114,6 @@ const result = CronScheduleSchema.parse(data);
114114
| **at** | `string` || ISO 8601 datetime when to execute |
115115

116116

117-
---
118-
119-
## RetryPolicy
120-
121-
### Properties
122-
123-
| Property | Type | Required | Description |
124-
| :--- | :--- | :--- | :--- |
125-
| **maxRetries** | `integer` || Maximum number of retry attempts |
126-
| **backoffMs** | `integer` || Initial backoff delay in milliseconds |
127-
| **backoffMultiplier** | `number` || Multiplier for exponential backoff |
128-
129-
130117
---
131118

132119
## Schedule

content/docs/references/system/meta.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"collaboration",
4646
"doc",
4747
"---More---",
48-
"metadata-types"
48+
"metadata-types",
49+
"retry-policy"
4950
]
5051
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
title: Retry Policy
3+
description: Retry Policy protocol schemas
4+
---
5+
6+
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
7+
8+
## TypeScript Usage
9+
10+
```typescript
11+
import { RetryPolicySchema } from '@objectstack/spec/system';
12+
import type { RetryPolicy } from '@objectstack/spec/system';
13+
14+
// Validate data
15+
const result = RetryPolicySchema.parse(data);
16+
```
17+
18+
---
19+
20+
## RetryPolicy
21+
22+
### Properties
23+
24+
| Property | Type | Required | Description |
25+
| :--- | :--- | :--- | :--- |
26+
| **maxRetries** | `integer` || Retry attempts after the initial one. 0 (the default) means no retry — state a count to opt in. |
27+
| **backoffMs** | `integer` || Base delay before the first retry (ms); subsequent delays multiply by backoffMultiplier |
28+
| **backoffMultiplier** | `number` || Exponential backoff multiplier; 1 (the default) keeps the delay flat |
29+
| **maxRetryDelayMs** | `integer` || Ceiling for a single backoff delay (ms) |
30+
| **jitter** | `boolean` || Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries |
31+
| **retryDelayMs** | `any` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661) — the retry policy now has one spelling for its base delay across `job.retryPolicy` and a `try_catch` node's `retry`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. `os migrate meta --from 16` rewrites it for you. |
32+
33+
34+
---
35+

docs/audits/2026-07-unknown-key-strictness-ledger.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,15 +477,15 @@ tightening (the #4001 "sharing-rule lesson": candidates, not verdicts).
477477
| `validation.zod.ts` | 6 | authorable | **strict as of #4001 batch 3b** — a `z.lazy()` discriminated union, so the one-call conversion does not apply: each of the six variants builds its own `strictObject` from a shared `BASE_VALIDATION_SHAPE`. Closing the base alone would have rejected correctly but suggested from the SHARED keys only, so a typo of a variant's own key (`transtions``transitions`) would get no rename. Site count 1 → 6 because the six variants are now object sites in their own right. The ADR-0010 envelope lives in the shared shape, so all six inherit it |
478478
| `field-value.zod.ts` / `seed.zod.ts` | 1+1 | mixed (p) | `seed` is strict (registered-types batch) |
479479

480-
### `automation/`88 sites
480+
### `automation/`87 sites
481481

482482
| File | Sites | Class | Note |
483483
|---|---|---|---|
484484
| `flow.zod.ts` | 11 | authorable | **strict as of #4001** (4 schemas; `FlowVersionHistorySchema` is runtime — stays tolerant) |
485485
| `sync.zod.ts` / `etl.zod.ts` | 12+10 | authorable (p) | authored pipelines — **candidates** |
486486
| `execution.zod.ts` | 13 | wire | run-state envelopes — never strict. +5 at #4354 (the run-summary family: step metrics / skip reason / per-node / per-gate / the summary itself) — engine-emitted telemetry read by the Console and by operator queries, nobody authors them, so the `wire` verdict covers them unchanged |
487487
| `state-machine.zod.ts` | 7 | authorable (p) | |
488-
| `control-flow.zod.ts` | 6 | authorable (p) | validated structurally by `validateControlFlow` |
488+
| `control-flow.zod.ts` | 5 | authorable (p) | validated structurally by `validateControlFlow`. **−1 at #4661**: `RetryPolicySchema` moved out to `shared/retry-policy.zod.ts``./automation` and `./system` published the same name for two different declarations (#4411), so the retry policy converged onto one. The site still exists and is still non-strict and authorable; it is simply no longer in a directory this ledger sections. ⚠️ That is a coverage gap worth knowing about: this audit sections `ui/` / `data/` / `automation/` / `security/` / `studio/` only, so a `shared/` shape is unaudited by construction. The tolerance is deliberate here — the `retryDelayMs``backoffMs` rename is tombstoned via `retiredKey()` precisely because a non-strict parent would otherwise swallow the old spelling |
489489
| `bpmn-interop.zod.ts` | 5 | wire (p) | interop import shapes |
490490
| `approval.zod.ts` | 4 | authorable | **strict as of #4001 step 3** — all four authoring schemas (node config / approver / escalation / decision-output). The published JSON schema carries `additionalProperties: false` into the Studio form AND `registerFlow()` config validation (#4027/#4040), so an unknown key in an approval node's `config` is rejected at registration too — verified: `z.toJSONSchema` on the strict lazySchema does not throw (#3746 hazard checked) |
491491
| `node-executor.zod.ts` | 4 | wire | executor contract |

0 commit comments

Comments
 (0)