Repository navigation
feat(spec)!: a job retryPolicy with no attempts is refused at maxRetries; retry-policy-converged leaves the load path (F2 of #23543) - #23810
Conversation
…licy-converged from the load path (F2 of #23543) Claude-Session: https://claude.ai/code/session_01MT2aGLQQhH8ZsPrgqHMrwz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MT2aGLQQhH8ZsPrgqHMrwz Co-authored-by: Claude <noreply@anthropic.com>
…empts is refused Claude-Session: https://claude.ai/code/session_01MT2aGLQQhH8ZsPrgqHMrwz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MT2aGLQQhH8ZsPrgqHMrwz Co-authored-by: Claude <noreply@anthropic.com>
…e load path Claude-Session: https://claude.ai/code/session_01MT2aGLQQhH8ZsPrgqHMrwz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MT2aGLQQhH8ZsPrgqHMrwz Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MT2aGLQQhH8ZsPrgqHMrwz Co-authored-by: Claude <noreply@anthropic.com>
…am and the authoring refusal Claude-Session: https://claude.ai/code/session_01MT2aGLQQhH8ZsPrgqHMrwz Co-authored-by: Claude <noreply@anthropic.com>
…-retry-policy-refused
📓 Docs Drift CheckThis PR changes 2 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 142 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 7278abae976b2551c2ec687f76d7bf73782990be && git checkout 7278abae976b2551c2ec687f76d7bf73782990be
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f846cc20202e580fa477e01a9cf11adac106d098 27418495868fe1fbcdf314d39efb280956560b1f && git checkout -B drift-repro f846cc20202e580fa477e01a9cf11adac106d098 && git merge --no-ff 27418495868fe1fbcdf314d39efb280956560b1f
node scripts/docs-audit/affected-docs.mjs --json f846cc20202e580fa477e01a9cf11adac106d098
|
席位核对 · ACCEPT · PR #23810 @
|
Contract reviewServed-tier: Inputs: card #23543 (body, ruling ① Derived judgmentsEach accept-set or public-surface change the diff implies, judged against the ruling's letter.
② Semver level
③ Boundary flagsDev flags (report
Escalated: nothing blocking. Two notes for the lane seat, neither a gate failure:
Gates read on the head: Implemented-by: VERDICT: PASS Generated by Claude Code |
…omit the count Claude-Session: https://claude.ai/code/session_01MT2aGLQQhH8ZsPrgqHMrwz Co-authored-by: Claude <noreply@anthropic.com>
…-retry-policy-refused
Part of #23543
Clause-②: yes (narrowing)
Regen-provenance: since the reviewed head 0bc4f42, dce2266 rewords the D3 entry's
reason(prose only, one file, at the seat's request) and 2741849 merges origin/main at f846cc2 through os-regen-merge.sh with check:generated clean; every other file's change equals the reviewed head's.This PR lands family F2 of ruling
6182001882(item 2, the loud form) only. F4 and F5-http stay on #23543, one PR each, in the ruled order; #23543 remains open.What
Ruling F2 A: "a
job.retryPolicyblock whosemaxRetriesis omitted or 0 is refused, in the shapeflow.errorHandlingalready uses forstrategy: 'retry'… A job with no block stays a single attempt;backoffMultiplierkeeps its declared default and is not refused.retry-policy-convergedthen setsretiredFromLoadPath; the 17 materialization lives on in the D3 chain."RetryPolicySchema(shared/retry-policy.zod.ts) is also atry_catchnode'sretryregion, which the ruling does not cover. SoJobSchemagains one root-level refine,job.retryPolicy === undefined || job.retryPolicy.maxRetries >= 1, reporting acustomissue atretryPolicy.maxRetrieswith the ruling's prescription as its first sentence: "A retry block with no attempts is no retry block: statemaxRetries≥ 1, for example 3, or delete the block." It then names why (the loader used to writemaxRetries: 3; a retry re-runs whatever the failed attempt did) and ends with the houseos migrate meta --from 16sentence. AtdefineStackthis isSTACK_SCHEMA_INVALID/ 422 atjobs.N.retryPolicy.maxRetries.dropped-refinements.baseline.jsonalready records forsystem/Job(thepullexclusivity refine). A field-level refine would have added a ledger site tosystem/Joband to every schema that embeds a job.retryPolicydescribe no longer says "declaring it withoutmaxRetriesalso means no retry"; it says a block must statemaxRetries>= 1.content/docs/references/system/job.mdxis regenerated from it.retry-policy-convergedgetsretiredFromLoadPath: trueandretiredAfter: '17.7.0', the stamp F1, F5-jsx and F3 used. Its "recorded verdict at 18" paragraph is rewritten as the retirement record. The fixture is unchanged: everyincludeRetiredseam still replays the whole entry.18.job-retry-policy-without-attempts-refused(semantic, step 18) withconversionIds: ['retry-policy-converged'], already a step-17 member. It states the decision no conversion makes for the author: keep the written count, or delete the block. It also covers the one silent change the ruling accepts: a block that statesmaxRetriesand omitsbackoffMultipliernow gets 1 where the loader wrote 2.conversions.test.tsmoves the entry to the retired list. The kept list now holds three entries:flow-node-http-callout-rename,page-component-visibility-to-visibleWhenandview-visibleOn-to-visibleWhen. The removal-version pin'sJOINgains oneretry-policy-convergedrow: the job refusal, which names@objectstack/spec 18and--from 16.Mechanism assumptions, measured
RetryPolicySchema. It holds. Its parsers arejob.retryPolicy(system/job.zod.ts) and atry_catchnode'sretry(automation/control-flow.zod.ts).flow.errorHandlingbuilds fromretryPolicyShape(), andworker.zod.tshas its ownTaskRetryPolicySchema. So the refusal is onJobSchema. A control pins thatRetryPolicySchema.safeParse({ backoffMs: 5000 })still succeeds.JOINrow is owed, becauseretiredAfter17.7.0 is the newest published stable release.retryDelayMshalf. It needs nothing beyond the retirement. Both surfaces carry theretryDelayMstombstone, and a new case inconversions.test.tspins an authored flow-levelretryDelayMsrefused atdefineStacknamingbackoffMs. Two existing tests relied on the load converting it, and both were re-pointed:flow-builtin-node-config-keys.test.ts"CONTROL: a pre-17 retryDelayMs alone is converted first where the door converts" now reads the replaying seam (applyConversionsToFlow(…, { includeRetired: true }), which parses) and the authoring door (defineStack, refused atconfig.retry.retryDelayMsnamingbackoffMs).retry-policy-key-validate-door.test.ts"declared" fixture now writesbackoffMs.6205630082). The measurement that decides them:os migrate metareplays fromMIGRATION_SUPPORT_FLOOR16, so--from 16replays steps 17 and 18, and after F2 every entry of both is retired. The only entries still live on the load path are toMajor 11 and 15 (F4's two, F5-http's one), and the chain never replays those. So no live entry can be both converted by the load and listed by the chain. Per suite:packages/spec/src/stack-authored-input.test.tsgets a synthetic probe. Its subject is the load pass itself, not an entry, so avi.mockof./conversions/registryappends a test-owned live conversion (stack-authored-input-probe, the oldretryDelayMsrename). Every assertion is unchanged and now reads the probe's id. No later family has to re-point this suite again.packages/cli/test/migrate-meta-load-conversions.test.tsis partly retired and partly re-pointed. A probe cannot reach it: the temp project loads the built@objectstack/specthrough a symlinkednode_modules, outside vitest's module graph.defineStackfills the declared defaults (backoffMultiplier: 1,maxRetryDelayMs: 30000,jitter: false), and the authored argument does not have them. I measured this on the built dist before writing it.retryDelayMsnow lives. The mixed case was retitled "two refused spellings the chain converts".packages/cli/test/migrate-meta-composed-load-conversions.test.tsgets the same treatment, for the package body.packages/cli/test/migrate-meta-composed.test.ts(its load-path row) is kept and renamed. Its assertions still hold, but its name was false. It is now "a refused stack carrying a second refused spelling".examples/**,packages/**,content/docs/**andskills/**for every object-literalretryPolicyblock, single- and multi-line, plus non-literal values. No job block omitsmaxRetriesor writes 0. The hits arehook.retryPolicy(a separate declaration, outside the ruling) and this PR's own fixtures. One test fixture wrotemaxRetries: 0on a job (job.test.ts, "one-time scheduled job"). It means a single attempt, so the block was deleted.skills/**has nothing to report.origin/mainmoved 5 commits (none in this PR's files) and was merged throughscripts/pm/os-regen-merge.shwith no conflict.check:generatedthen read "All 14 generated artifacts are up to date".protocol.ts) is untouched. For a non-flow type it judges the body as stored, with no conversion replay, so a job body meets this refusal there.jobhas not been runtime-creatable since 17, though, so no door claim is made for it. The flow half (retryDelayMs) meets the door's flow fallback exactly as before, because that fallback already replayed this entry while it was live. The seat's separate save-door decision covers it with the other retired flow entries.Readers updated (prose made false by the retirement)
content/docs/automation/jobs.mdx: "declaringretryPolicywithout stating a count still means no retry" is now the refusal rule.packages/services/service-job/src/run-with-policy.ts(comment only): the default is reached only by a hand-built caller.shared/retry-policy.zod.ts(two comments),liveness/flow.json(one note) and thejob-timeout-to-timeout-msdocblock inregistry.tsno longer call the entry a load-path rewrite.Changeset
.changeset/23543-f2-job-retry-policy-without-attempts-refused.md:@objectstack/specminor;@objectstack/service-jobpatch (comment-only).Clause-②: yes (narrowing). It gives FROM → TO:retryPolicy: {}→retryPolicy: { maxRetries: 3 }or delete the block, and a block withmaxRetries: 0→ delete it. It has a "What an author who still writes it sees" paragraph, thebackoffMultiplier2 → 1 note, theos migrate meta --from 16path, and how stored rows keep loading.registered job-retry-policy-without-attempts-refused.Verification
All readings are on head
0bc4f428(the merge oforigin/main46ca793e) unless noted.@objectstack/spec: fulltest683 files / 20888 passed (1 todo), exit 0.typecheckexit 0 (pre-merge9e09e0dd; the merge brought no change to this PR's files).build+check:generated: "All 14 generated artifacts are up to date".@objectstack/cli: the fullunittier passed, 297 files. The first run had 295 passing, plus the 2 build-dependent published-subpath pins, which failed only with "packages/cli is not built" and passed 2 / 29 afterpnpm --filter @objectstack/cli build. 4348 tests passed, 29 skipped. Integration tier: the one edited file,retry-policy-key-validate-door.test.ts, 2 / 2.typecheckexit 0, after building the closure with declarations.@objectstack/service-job:test11 files / 117 passed.typecheckexit 0.dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 114 commands on0bc4f428. Each ran one by one, with its exit code captured before any pipe.--ranreports "114 derived famil(ies) accounted for — 111 run, 3 NOT-MEASURED (3 DERIVED from a recorded exit 3)". The three are the prerequisite refusals F3 also recorded:check-plugin-teardown-shape --self-test(shallow clone),check:skill-examples(client dist absent) andcheck:dual-build-cjs-loads(full build absent). Notable greens:check-adr-0087-registration --base origin/main,check-changeset-no-major,check-empty-changeset,check:dts-closure,check:nul-bytes,check:liveness,check:spec-changes,check:upgrade-guide.scripts/ablation-replace.mjs, on9e09e0dd).job.retryPolicy.maxRetries >= 1was replaced by>= 0.28e227b31f4e→c6c4d7684caa.job.test.ts(3),conversions.test.ts(3, including the stored-row case's "the row as written is refused" leg) andretired-key-removal-version.pin.test.ts(theretry-policy-convergedJOINrow). Direction: red.git diff HEADempty. Spec tests importsrc, so there is no dist leg.Acceptance notes
label-write, so this PR carries none from me.tscdoes not see the omission:maxRetriesstays optional inRetryPolicy, because the shared input type also servestry_catch. The parse-time prescription is the channel for this family.Generated by Claude Code