Uh oh!
There was an error while loading. Please reload this page.
feat(service-automation,qa): surface a failed try-region's steps so a caught failure leaves a trace (#7546) - #7699
Conversation
… caught failure leaves a trace (#7546) A caught `try_catch` failure used to record nothing about the failure. The run log was `[start, container(success), catch-step]`: no step carried `regionKind: 'try'`, none carried `status: 'failure'`, and the container's own step read `success`, so a caught failure was indistinguishable from a clean run that merely touched the catch path. The only evidence was the catch region's side effects. The steps were never structurally missing — a failing node pushes its own `failure` step into the region's array before it throws, and the #1479 `childSteps` splice already carries region steps into the parent log. The failed attempt's array was simply discarded as the region unwound. `runRegion()` now hands a failed region's partial steps to the caller through an opt-in sink before the throw propagates, tagged exactly as a successful region's are; `try_catch` accumulates every failed attempt across the retry ladder and folds them into `childSteps` ahead of the surviving region's steps. With a retry policy declared they also carry `retryAttempt`, making the attempt count a value in the log rather than an inference from elapsed time — a key the spec's `ExecutionStepLogSchema` has declared all along with no producer until now. Retry and throw semantics are unchanged, and a recovered container still reports `success`; a distinct `recovered` status was considered in the decision and not adopted. The run-summary fold needs no special case: a node that failed twice then succeeded folds to `runs: 3, failures: 2`, all true, and rows written by an abandoned attempt now reach `selected`/`acted` instead of vanishing. Also rewrites the `flow-error-handling` checklist item (revision 2) — it asserted the failing try step's presence, a contract the engine had never promised, which is why QA run #7516 read a by-design silence as a FAIL. The ruling implemented the clause rather than weakening it, so the item now pins the new contract, the container's by-design `success`, and a retryAttempt count in place of the weak duration-only retry oracle. Ruling: #7546 (maintainer, 2026-08-11).
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also reference the affected code. These are read-only:
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#7546
Implements the maintainer ruling on #7546 — surface the failed try-region's steps, not the card's own Option C recommendation.
(Ruling comment of 2026-08-11, recorded from the maintainer's chat instruction 「接受你的全部建议,请更新 issue 的状态和标签」.)
Premise verification
Re-checked on this PR's base,
origin/main@779ace4— all five hold. Line numbers moved from the triage anchors taken at34c01a5; nothing else did.engine.ts:5367(triage said 5315), ending "a failed attempt's partial steps are not surfaced"try-catch-node.tsreturnschildStepsonly from a region that succeededregionKind: 'try'tagging attry-catch-node.ts:109; the failed attempts' steps were dropped on the floor in thecatch (err)armchildStepsinto the parent logengine.ts:5127,if (result.childSteps?.length) steps.push(...)summarizeRunis a pure fold inrun-summary.tsOne correction worth recording, because it changes what the fix had to build: the failing node's step already existed.
executeNodepushes astatus: 'failure'step into the region's array before it throws, so the material the card wanted was being assembled and then discarded as the region unwound. Nothing needed to be synthesised — only handed back.What changed
engine.tsrunRegion()takes an optionalpartialStepssink. On failure it tags the partial steps exactly as the success path does, pushes them into the sink, then rethrows unchanged. A sink rather than a return value because this path's contract is still "throw" — carrying steps out through the exception would either change what callers catch or need a bespoke error type, both larger seams. The two callers that do not opt in (loop,parallel) are untouched.status.StepLogEntrygainsretryAttempt. Not new vocabulary — the spec'sExecutionStepLogSchemahas declaredretryAttempt("Retry attempt number (0 = first try)") since it was written, and had no producer anywhere in the engine. This gives the declared key its first writer, which is the direction declared-equals-enforced asks for. Nopackages/specfile is touched.try-catch-node.ts— accumulates each failed attempt's steps across the retry ladder and folds them intochildStepsahead of the surviving region's steps, on both the catch path and the path where a retry eventually succeeds. The attempt index is tagged only when a retry policy is actually declared; otherwise every step would carry a constantretryAttempt: 0, which is noise rather than signal.run-summary.ts— doc only, no code guard. See the P4 note below.docs/qa/platform-checklist/areas/automation.json—flow-error-handlingrewritten, revision 1 to 2 with the matching history entry. Theflow-node-type-matrixitem (#7664) is untouched; verified by structural diff, exactly one item changed.Two judgement calls, flagged for review
childStepsonly on a successful node result, so attaching them to a failing one is dead weight; surfacing there would mean changing the splice for every container kind. That path is also not the gap this card is about — an unhandled failure already terminates the runfailedwith both run-level and step-level errors, which the QA run rated as passing.P4: the fold needs no guard
The PM brief allowed a small guard in
run-summary.tsif retried attempts inflated per-noderunsmisleadingly. They do not. A try node that failed twice before succeeding folds toruns: 3, failures: 2, status: 'failure'— every number true, and the same "worst outcome wins,runs/failurescarry the nuance" rule a loop body has always folded under; a retry ladder is just another way for one node to run more than once. The node-levelfailuredoes not propagate to the run, so a recovered container still yields a completed run. Metrics get strictly more accurate: rows written by an attempt that then threw now reachselected/actedinstead of vanishing. Documented in the fold's header and pinned by two tests, rather than guarded in code.Container status was not touched — that was Option C's
recoveredidea, which the ruling did not adopt. A test pins it that way so it cannot drift.Verification
pnpm --filter '@objectstack/service-automation^...' build— dependency closure built first in a fresh worktree.pnpm --filter '@objectstack/service-automation' build— green (tsup DTS is this package's typecheck; it has notypecheckscript).pnpm --filter '@objectstack/service-automation' test— 75 files, 920 tests, all passing, including the 10 pre-existingtry-catch-node.test.tscases andnested-region-parity/nested-composition.try-catch-failed-attempt-steps.test.ts, 9 cases — what failed, how many attempts (retryAttempt 0,1,2), which node threw, the whole partial attempt rather than only the throwing node, the retry-succeeds ladder, "a recovered container still reports success", no attempt index without a retry policy, a clean run staying clean, and the two run-summary fold assertions.Gates, individually:
node scripts/check-nul-bytes.mjsnode scripts/check-platform-checklist.mjscoverage.json qa: UNCLASSIFIED(#7347). The edited item validatesnode scripts/checklist-select.mjs --self-testpnpm check:docs-audit-scopenode scripts/check-changeset-fixed.mjscheck:engine-double-contractis not implicated — no new fake or stub data engine; the test doubles here are flow node executors.Reverse verification
Predicted before running: disabling the single seam (
partialSteps?.push(...)inrunRegion's catch arm) should turn 7 of the 9 new tests red and leave exactly two green — "a recovered container still reports success" and "a clean run stays clean", neither of which depends on a failed attempt being recorded — while all 10 pre-existingtry-catch-node.test.tscases stay green, since retry and throw semantics are untouched.Measured:
Tests 7 failed | 12 passed (19)— the 7 predicted failures, those exact 2 new tests green, and all 10 pre-existing cases green. Prediction matched exactly. Restored by inverse edit (nogit stash; the marker line is absent and the push line is back, both confirmed by grep before committing).The pre-existing suite staying green under the disable is the load-bearing half: it is what shows this change adds records without moving behaviour.
Notes
@objectstack/service-automationminor — additive log records, no breaking change, so no ADR-0087 disposition line is required.packages/specedits; nocontent/docs/releases/edits.Generated by Claude Code