fix(approvals): keep an undifferentiable stranded row in the report, and stop a malformed host verdict aborting the scan - #16739
Conversation
…and stop a malformed host verdict aborting the scan Three residues of the #15358 contract review (#16709). Item 1 (test-only) — the restore verb's drop of a stale hot consumed-suspension copy was unpinned package-wide: the reviewer's E2 ablation deleted the behaviour and left the whole service-automation suite green. Pinned where the drop is distinguishable from a no-op — after the durable row that proves the copy stale is evicted by run-history retention, a kept copy would be the only witness left and would offer an operator a restore of a run that already COMPLETED on another replica. Item 2 (PM ruling, 2026-09-08) — a thrown third read counted `undetermined` and dropped the row. By the time that oracle is asked the first two have already answered (no live pause, terminal `failed`); it is asked only WHICH of the three shapes the row is, so a read that could not be made is exactly the "could not differentiate" case `'failed'` already means. The row now stays in the report; `undetermined` is kept as telemetry. Item 3 — `refineFailedRunState(verdict)` ran outside the `try`, so a host resolving `undefined` threw a `TypeError` out of `inspectStrandedRequests` and the scan enumerated nothing. The refinement now runs inside that `try`: a malformed verdict costs its own row the differentiation and no other row anything. No new `StrandedRunState` member and no widened export. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
…rand-inspection-residues
📓 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:
What this run could not see
Coarse fallback — 6 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 7dc2545a2c9e97809d6e2664c086a4622fee2cd2 && git checkout 7dc2545a2c9e97809d6e2664c086a4622fee2cd2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1ea349f0ebc4b9a0090af03ecbdea9b1abcef871 7e5b34c444d6f56635cb0d07d35257e9dbc976d3 && git checkout -B drift-repro 1ea349f0ebc4b9a0090af03ecbdea9b1abcef871 && git merge --no-ff 7e5b34c444d6f56635cb0d07d35257e9dbc976d3
node scripts/docs-audit/affected-docs.mjs --json 1ea349f0ebc4b9a0090af03ecbdea9b1abcef871
|
Fixes #16709
The three §5 non-blocking residues of the #15358 contract review, taken together. Item 2 is executed per the PM seat's ruling on the card (comment
5577982860): a thrown third read reports'failed', it does not leave thestrandedlist.Every line number and count on the card was measured at head
1b4dad617, before #16704 landed. All of it was re-measured here againstorigin/main.Item 1 — the stale-hot drop, pinned, and shown to fail first
restoreConsumedSuspensiondrops a hot consumed-suspension copy the durable row proves stale, in one line behind one helper flag. The reviewer's ablation E2 deleted that behaviour and the whole package stayed green.New file:
packages/services/service-automation/src/stale-hot-consumed-suspension.test.ts.The pin is written on the outliving, because that is the only place the drop is distinguishable from a no-op: with the terminal row still present, a dropped and a kept hot copy both answer
NO_CONSUMED_SUSPENSION(the row supersedes the copy on every read). So replica A strands a run, replica B restores and finishes it, A's own restore attempt reads the row and drops its copy — then run-history retention evicts the row, and a kept copy would be the only witness left, answeringrepairable: true, witness: 'journal'for a run that already COMPLETED. Two pins: the read-only verb answersNO_CONSUMED_SUSPENSION, and the restore verb refuses instead of re-arming a finished run.Red first — E2 re-run against the new pin. Mutation: all three
staleHot: hot !== undefinedinresolveConsumedSuspensionWitnessestostaleHot: false.45dbeba18to8033426867dist/ablation-dist-preflight.mjs @objectstack/service-automation 'staleHot: hot !==' --absentexit 0 — "marker absent from all 6 built files"45dbeba18, whole-treegit status --porcelainempty, preflight in present mode exit 0, suite back to 1489 passedThat reproduces the reviewer's reading exactly (every pre-existing test stayed green under E2) and shows the new pin is the thing that notices. The file's third test is a deliberate CONTROL and correctly stays green under E2: it drives a store with no run history at all, where nothing can ever supersede the hot copy.
Item 2 — a thrown third read stays in the report
Per the ruling. The catch arm keeps
undetermined++as telemetry and no longercontinues, so the row is reported as the undifferentiated'failed'.The reason it is not the same call as the two earlier oracles is in the code and in the changeset: a throw from
hasSuspendedRunorgetRunleaves it unknown whether the row is stranded, and a storage outage must not be published as a lost run. By the time the third oracle is asked, both have answered — no live pause, terminalfailed— and it is asked only which of the three shapes. A read that could not be made is the textbook "could not differentiate", which is what'failed'already means under the #15358 ruling's item 1.⛔ No sixth
StrandedRunStatemember; the union is untouched at five.Item 3 — a malformed host verdict no longer aborts the scan
refineFailedRunState(verdict)moved inside thetrythat wraps the read. A host resolvingundefinedused to throwTypeError: Cannot read properties of undefined (reading 'repairable')out ofinspectStrandedRequests, so the scan enumerated nothing. It now costs its own row the differentiation and every other row nothing.The pins for items 2 and 3, and their ablations
packages/plugins/plugin-approvals/src/stranded-request-inspection.test.ts: the existing thrown-read test is re-pinned to the ruled behaviour (it used to assertexpect(out.stranded).toEqual([]), and the comment says so), plus a new#16709block — the malformed-verdict case, a mixed population where one thrown and one malformed row cost only themselves a label, a control that item 2 did not widen to the two earlier oracles (they still skip), and a pin that every label emitted is still one of the five members.Both ablations proved on disk and in
dist/(ablation-dist-preflightexit 0 on each mutate leg), restored with the blob hash back at HEAD and a clean whole-tree status:differentiated = false;removed,continue;restored in the catchtryTypeErrorin the failure text; item 2's thrown-read pin stays green, which is the attributionClause ② —
no, derived from the real export surfaceNot from a path test.
@objectstack/plugin-approvals' publisheddist/index.d.tswas built atorigin/mainand at HEAD and compared:diffexit 0 — byte-identical published type surface.scripts/pm/check-widening-tells.mjs --declaration noovergit diff origin/main...HEAD: no widening tell on any declared surface (its own--self-test, 131 cases, green first).@objectstack/service-automationhas no source change at all — item 1 adds one.test.tsand nothing else.StrandedRunStatekeeps its five members;ApprovalResumeSurfaceis unchanged; no barrel is in the change set.Changeset
@objectstack/plugin-approvalspatch —AGENTS.md:1029verbatim: a bug fix in a released package takes a patch changeset, neverskip-changeset. Item 3 changes the runtime behaviour of a released package and item 2 changes what the report contains, so "it is mostly tests" does not apply.@objectstack/service-automationis not named: it publishes nothing from this diff.Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 58 families; all 58 were run and--ranreconciles 58 derived / 58 run / 0 UNRUN. 57 exit 0.The derivation was first taken on the pre-merge tree and reported STALE TREE;
origin/mainwas fetched and merged into this branch, and the derivation redone at7e5b34c44.pnpm check:i18nandpnpm check:type-check-debtfirst answeredexit 3PREREQUISITE NOT MET. Both were cleared locally by building their stated closure, and both then exit 0.pnpm check:dual-build-cjs-loadsstaysexit 3— it reads built output for 12 packages this diff does not touch (studio,client-react, the connectors, the embedders) and its prerequisite is a whole-treepnpm build, which CI runs as its own step. Read as NOT MEASURED locally, not as a pass.Package verification, at the final commit
7e5b34c44:@objectstack/service-automation126 files / 1489 tests passed,@objectstack/plugin-approvals42 files / 709 tests passed;typecheckgreen for both.验收备注
undeterminedcounter andstrandednow overlap on purpose — a third-read failure is both reported and counted. Stated in the field's own TSDoc and in the changeset, because neither number alone sizes the scan's blind spot any more.🤖 Generated with Claude Code
https://claude.ai/code/session_012zTkyNHJ7TkuN2oXtP5x37
Generated by Claude Code