Skip to content

docs(spec): a durable pause ends the retry-governed segment - #10173

Merged
qq9340100 merged 2 commits into
mainfrom
claude/issue-9705-pause-ends-retry-segment
Aug 20, 2026
Merged

docs(spec): a durable pause ends the retry-governed segment#10173
qq9340100 merged 2 commits into
mainfrom
claude/issue-9705-pause-ends-retry-segment

Conversation

@qq9340100

Copy link
Copy Markdown
Collaborator

Fixes#9705

A durable pause ends the retry-governed segment. errorHandling.strategy: 'retry'
describes ONE synchronous dispatch: when a run parks on an approval, screen or wait
node (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
retry as covering the run, and the post-approval half was unprotected silently. This PR
states 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 changeengine.ts is untouched, the
accepted flow set is unchanged, and validation is byte-identical.

What changed

FileChange
packages/spec/src/automation/flow.zod.tsThe boundary and its authoring consequence on the errorHandling block's .describe() and on strategy's; the rationale (and the pin's location) in the block JSDoc
content/docs/references/automation/flow.mdx, content/docs/references/api/automation-api.mdxRegenerated projections of that block describe (pnpm --filter @objectstack/spec gen:schema && gen:openapi && gen:docs) — not hand-edited
content/docs/automation/flows.mdxNew normative subsection "A durable pause ends the retry-governed segment" (#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 section
content/docs/automation/approvals.mdxThe same pointer at the lifecycle step where the run resumes — where an author actually meets it
packages/services/service-automation/src/retry-attempt-pause.test.tsThe #9510 pin retitled from measured "current behaviour" to the ruled contract. Comment and test title only
.changeset/pause-ends-retry-segment.md@objectstack/spec patch

The authoring recipe, since that is the deliverable's point

Flow-level retry cannot reach the post-pause half, so the docs prescribe giving that half
its own failure handling inside the flow: a try_catch node with its own retry
around the post-resume work, or a fault edge to a handler node. Both are per-node and
local 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 — backoffMs and backoffMultiplier are delays
the engine sleeps through, jitter spreads a thundering herd — and a pause of arbitrary
duration (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:
SuspendedRun declares no attempt field (nothing to inherit), and resumeInternal never
consults 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_run migration, and an answer for a flow republished mid-pause with a
different maxRetries.

The pin is now a contract pin

The it() title and the JSDoc above it now say the behaviour is ruled rather than merely
current, 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:

- it('answers the retry-budget question: a resumed run does not retry, on either route', async () => {
+ it('pins the RULED contract — a durable pause ends the retry-governed segment: a resumed run does not retry, on either route', async () => {

Verification

Gates re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs (no paths
passed — 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 syncing main. Every
one green.

GateVerdict line (the gate's own, not $?)
check:doc-anchors✅ 252 internal #fragment link(s) across 399 source file(s) all resolve to a real heading — covers the new #retry-pause-boundary id and both links to it
check: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 loudly
check: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-changesetall exit 0
check-adr-0087-registration.mjs, check-changeset-no-major.mjs, check-empty-changeset.mjs, check-dev-prereqs.mjs, docs-audit/check-affected-docs.mjsall exit 0
@objectstack/speccheck:variant-docs, check:strictness-ledger, check:empty-state, check:liveness, @objectstack/lintcheck:doc-formula-expressionschain exit 0 — ✓ every governed-type property … is classified

Tests

One declared narrowing.check:type-check-debt --re-measure re-runs tsc for all 33 ledger
entries 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-worktree
refusal. 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 as
tscErrorCount() invokes it):

packages/services/service-automation/src/nested-region-parity.test.ts(95,25): error TS2341: …
packages/services/service-automation/src/nested-region-parity.test.ts(151,20): error TS2341: …
packages/services/service-automation/src/nested-region-parity.test.ts(180,20): error TS2341: …

3 raw errors, equal to the frozen errors: 3 — no drift, and none of the three is in the file
this PR edits. CI runs the full ratchet regardless.

Scope

Clause-②: no — the accepted flow set does not change and validation stays
byte-identical. engine.ts untouched; no part of Option B implemented. content/docs/
synced with main before opening (the webhooks-headers churn there is disjoint from these
two pages).


Generated by Claude Code

`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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 1 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/flows.mdx(via FlowSchema (symbol))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via FlowSchema (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 115 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 89f3654c853605a4ed85de3518410df814432ce0packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2b9f1b3a5c10af0116b3596820ebb090e19b4260 — the merge of head e42616477b265657e2a6a3b4b64b2eda1669c521 into base 89f3654c853605a4ed85de3518410df814432ce0, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 89f3654c853605a4ed85de3518410df814432ce0 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 20, 2026
@qq9340100
qq9340100 marked this pull request as ready for review August 20, 2026 13:24
@qq9340100
qq9340100 added this pull request to the merge queueAug 20, 2026
Merged via the queue into main with commit 4c93387Aug 20, 2026
26 checks passed
@qq9340100
qq9340100 deleted the claude/issue-9705-pause-ends-retry-segment branch August 20, 2026 14:04
qq9340100 pushed a commit that referenced this pull request Aug 20, 2026
…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>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@qq9340100@claude