Uh oh!
There was an error while loading. Please reload this page.
docs(spec): a durable pause ends the retry-governed segment - #10173
Conversation
`errorHandling.strategy: 'retry'` describes ONE synchronous dispatch. A run that parks on an `approval`, `screen` or `wait` node (ADR-0019) resumes as a new segment outside it, so anything that fails after the pause gets exactly one attempt. That was measured on the engine but stated nowhere an author reads. Ruled deliberate on #9705 (Option A, maintainer ruling 2026-08-18). Prose only: - `flow.zod.ts` — the boundary and the authoring consequence on the `errorHandling` block's `describe()` and on `strategy`'s, with the rationale in the block JSDoc; the generated reference projections follow. - `content/docs/automation/flows.mdx` — a normative subsection with the two-halves table, why the boundary is the contract rather than a gap, and the recipe for protecting the post-pause half (a `try_catch` node's own `retry`, or a `fault` edge); plus a pointer from the durable-pause section. - `content/docs/automation/approvals.mdx` — the same pointer where an author meets it, at the step the run resumes. - `retry-attempt-pause.test.ts` — the #9510 pin retitled from measured current behaviour to the RULED contract. Comment and test title only; every assertion is byte-identical. No validation change: the accepted flow set is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016gcKVsiywU9CcS96S5t9qD
…se-ends-retry-segment
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 115 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 2b9f1b3a5c10af0116b3596820ebb090e19b4260 && git checkout 2b9f1b3a5c10af0116b3596820ebb090e19b4260
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 89f3654c853605a4ed85de3518410df814432ce0 e42616477b265657e2a6a3b4b64b2eda1669c521 && git checkout -B drift-repro 89f3654c853605a4ed85de3518410df814432ce0 && git merge --no-ff e42616477b265657e2a6a3b4b64b2eda1669c521
node scripts/docs-audit/affected-docs.mjs --json 89f3654c853605a4ed85de3518410df814432ce0
|
Uh oh!
There was an error while loading. Please reload this page.
…10266) The `JobHandler` TSDoc's "not yet wired" paragraph was inverted twice over by #7072's landing: consuming a resolved `{ outcome: 'degraded' }` is no longer discarded, and "safe because unread" no longer holds. Re-measured against current main: - cron-job-adapter.ts and interval-job-adapter.ts map a resolved `degraded` outcome onto `execution.status = 'degraded'` / `execution.error`. - db-job-adapter.ts settles the run as `degraded`, writes the reason to `error` / `last_error`, and leaves `failure_count` flat. - sys_job_run.status and sys_job.last_status both carry `degraded` in their ObjectQL-enforced select vocabularies (#7072), pinned to stay in step with JobExecutionStatus. Rewrote the paragraph to state the wired behaviour, aligned in wording with JobExecutionStatus's already-correct TSDoc in system/job.zod.ts rather than inventing a third phrasing. The #5548/#6617 framing above stays untouched. Prose only — no signature or type change (Clause-②: no). No changeset: JSDoc/TSDoc block-comment change, not a Zod `.describe()` call that feeds generated reference docs — same precedent as #10168, not #10173. Claude-Session: https://claude.ai/code/session_016gcKVsiywU9CcS96S5t9qD Co-authored-by: Claude <noreply@anthropic.com>
Fixes#9705
A durable pause ends the retry-governed segment.
errorHandling.strategy: 'retry'describes ONE synchronous dispatch: when a run parks on an
approval,screenorwaitnode (ADR-0019), the continuation that resumes later is a new segment outside it, and
anything that fails after the resume gets exactly one attempt.
That was measured on the engine, but stated nowhere an author reads — so an author
combining a flaky connector call with an approval node reasonably read the flow-level
retryas covering the run, and the post-approval half was unprotected silently. This PRstates the boundary where authors meet it, per the maintainer ruling recorded on the card
(2026-08-18 22:30Z, Option A: 「其他接受你的建议」).
Prose only. No behaviour change, no engine change —
engine.tsis untouched, theaccepted flow set is unchanged, and validation is byte-identical.
What changed
packages/spec/src/automation/flow.zod.tserrorHandlingblock's.describe()and onstrategy's; the rationale (and the pin's location) in the block JSDoccontent/docs/references/automation/flow.mdx,content/docs/references/api/automation-api.mdxpnpm --filter @objectstack/spec gen:schema && gen:openapi && gen:docs) — not hand-editedcontent/docs/automation/flows.mdx#retry-pause-boundary): the two-halves table, why the boundary is the contract rather than a gap, and the authoring recipe; plus a pointer from the durable-pause sectioncontent/docs/automation/approvals.mdxpackages/services/service-automation/src/retry-attempt-pause.test.ts.changeset/pause-ends-retry-segment.md@objectstack/specpatchThe authoring recipe, since that is the deliverable's point
Flow-level
retrycannot reach the post-pause half, so the docs prescribe giving that halfits own failure handling inside the flow: a
try_catchnode with its ownretryaround the post-resume work, or a
faultedge to a handler node. Both are per-node andlocal to the segment that is running, so both work after a resume. The docs also make the
point that this is the better shape regardless — a flow-level retry re-runs the flow from
the start, which after an approval would open a second approval request.
Why the boundary is the contract and not a gap
The retry knobs model an in-process loop —
backoffMsandbackoffMultiplierare delaysthe engine sleeps through,
jitterspreads a thundering herd — and a pause of arbitraryduration (a human approval may sit for days, across a process restart) is not something
that model can honestly extend across. Measured in both directions on
origin/main:SuspendedRundeclares no attempt field (nothing to inherit), andresumeInternalneverconsults
flow.errorHandling(nothing would read it if it did).Option B — resume inherits the remaining budget — is the recorded revisit path only,
and no part of it is implemented here: it would need a field on the durable snapshot, a
sys_automation_runmigration, and an answer for a flow republished mid-pause with adifferent
maxRetries.The pin is now a contract pin
The
it()title and the JSDoc above it now say the behaviour is ruled rather than merelycurrent, so a future change to those assertions reads as a contract change. Verified
mechanically that every assertion is byte-identical — the only non-comment line in that
file's diff is the
it()title:Verification
Gates re-derived from the actual diff with
node scripts/pm/dispatch-gates.mjs(no pathspassed — the script takes its own change set from the merge base), which named six families
beyond the dispatch list once the regenerated reference pages and the changeset joined the
diff. The union below was run at
e42616477— the final head, after syncingmain. Everyone green.
$?)check:doc-anchors✅ 252 internal #fragment link(s) across 399 source file(s) all resolve to a real heading— covers the new#retry-pause-boundaryid and both links to itcheck:docs-audit-scope✓ docs-accuracy-audit scope is in sync with content/docs/: 180 hand-written doc(s).check:quick-reference-counts✓ every "(N of M schemas)" heading matches its table AND content/docs/references/check:engine-double-contractOK — 329 pinned, 133 in the DEBT ledger, 2 exempt.check:where-matcher✓ 264 matcher(s) discovered, 264 answer the combinator battery correctly or refuse it loudlycheck:type-check-coverageOK — 64/77 workspace packages type-checked …, 13 in the DEBT ledger (436 frozen raw errors), 1 exempt.(self-test: 109 cases hold)check:docs-redirects,check:published-readme-links,check:role-word,check:merge-driver,check:spec-parsed-alias,check:slot-lookup,check:test-source-alias,check:type-source-resolution,check:cross-package-test-inputs,check:query-options-erasure,check:changeset-gate-self-tests,check:objectui-changesetcheck-adr-0087-registration.mjs,check-changeset-no-major.mjs,check-empty-changeset.mjs,check-dev-prereqs.mjs,docs-audit/check-affected-docs.mjs@objectstack/speccheck:variant-docs,check:strictness-ledger,check:empty-state,check:liveness,@objectstack/lintcheck:doc-formula-expressions✓ every governed-type property … is classifiedTests
pnpm --filter @objectstack/service-automation test— 83 files / 991 tests passed, the retitled bug(service-automation): a retry attempt that PAUSES is recorded as failed and its suspension is never persisted —executeWithoutRetryhas noisSuspendSignalarm #9510 pin among them.pnpm --filter @objectstack/spec test— 414 files / 11040 tests passed.pnpm --filter @objectstack/spec run typecheck— clean (tsc --noEmit+ scripts + test layer).One declared narrowing.
check:type-check-debt --re-measurere-runstscfor all 33 ledgerentries sequentially and does not fit this session's foreground wall (it was cut off mid-run twice
after its closure build). The workspace closure was built first —
pnpm exec turbo run build --filter=./packages/* --filter=./packages/*/*, 70/70 tasks — so this is not the unbuilt-worktreerefusal. In its place I ran the gate's own measurement for the one ledger entry this diff can
move,
@objectstack/service-automation(tsc --noEmit --pretty false -p packages/services/service-automation/tsconfig.json, from the repo root, exactly astscErrorCount()invokes it):3 raw errors, equal to the frozen
errors: 3— no drift, and none of the three is in the filethis PR edits. CI runs the full ratchet regardless.
Scope
Clause-②: no— the accepted flow set does not change and validation staysbyte-identical.
engine.tsuntouched; no part of Option B implemented.content/docs/synced with
mainbefore opening (the webhooks-headers churn there is disjoint from thesetwo pages).
Generated by Claude Code