Found while implementing #14456 (the contained-failure visibility contract). Out of scope there and deliberately not widened into — filing it instead.
What is there now
#14456 populates FlowRunSummary.failed (the fold failed = Sigma nodes[].failures) and persists it. It rides inside the summary_json blob on sys_automation_run, including on a summary compacted past the 16 KiB cap, where the totals are kept and the per-node detail is dropped.
The four sibling totals each have a column of their own — selected_count, acted_count, skipped_count, unmeasured_count — and packages/services/service-automation/src/sys-automation-run.object.ts states why, in a comment right above them:
// COLUMNS, not just a blob: `selected_count > 0 AND acted_count = 0` is the
// first FILTER of the broken-sweep detector, and an operator can only alert
// on what is filterable. Buried inside `summary_json` these would be
// readable but not queryable — the difference between a dashboard and an
// alarm.
failed has no such column.
Why that may matter
The premise of the containment work is that a run which caught a per-iteration failure completes GREEN: status = 'completed', and no other stored column moves. So "show me the runs that lost rows this week" is not answerable from the columns at all — it is a LIKE scan over summary_json, or nothing. That is the same "readable but not queryable" gap the comment above was written against, one field over.
It is not obviously the same shape as the broken-sweep filter, which is why this is a finding and not a defect:
unmeasured_count exists to QUALIFY acted_count — it is read as part of one filter expression, so it has to be in the same row as its operands.
failed would be its own predicate (failed_count > 0), which a caller could equally get by parsing the blob it already fetches.
So the question is a real one and this card does not presume the answer.
Options
- Add
failed_count, mirroring unmeasured_count: null on rows written before the count existed (never 0 — absent means "not tracked", the convention the spec docblock spells out for both unmeasured and failed), written from record.summary?.failed ?? null beside its four siblings in ObjectStoreSuspendedRunStore.
- Leave it in the blob and say so in
summary_json's own description, so the next reader does not re-derive the question.
Option 1 is a stored-surface change on an ADR-0103 engine-owned object, which is why it was not taken as a rider on #14456.
Not in scope of this card
Filed from claude/issue-14456-contained-failure-visibility.
Found while implementing #14456 (the contained-failure visibility contract). Out of scope there and deliberately not widened into — filing it instead.
What is there now
#14456 populates
FlowRunSummary.failed(the foldfailed = Sigma nodes[].failures) and persists it. It rides inside thesummary_jsonblob onsys_automation_run, including on a summary compacted past the 16 KiB cap, where the totals are kept and the per-node detail is dropped.The four sibling totals each have a column of their own —
selected_count,acted_count,skipped_count,unmeasured_count— andpackages/services/service-automation/src/sys-automation-run.object.tsstates why, in a comment right above them:failedhas no such column.Why that may matter
The premise of the containment work is that a run which caught a per-iteration failure completes GREEN:
status = 'completed', and no other stored column moves. So "show me the runs that lost rows this week" is not answerable from the columns at all — it is aLIKEscan oversummary_json, or nothing. That is the same "readable but not queryable" gap the comment above was written against, one field over.It is not obviously the same shape as the broken-sweep filter, which is why this is a finding and not a defect:
unmeasured_countexists to QUALIFYacted_count— it is read as part of one filter expression, so it has to be in the same row as its operands.failedwould be its own predicate (failed_count > 0), which a caller could equally get by parsing the blob it already fetches.So the question is a real one and this card does not presume the answer.
Options
failed_count, mirroringunmeasured_count: null on rows written before the count existed (never0— absent means "not tracked", the convention the spec docblock spells out for bothunmeasuredandfailed), written fromrecord.summary?.failed ?? nullbeside its four siblings inObjectStoreSuspendedRunStore.summary_json's own description, so the next reader does not re-derive the question.Option 1 is a stored-surface change on an ADR-0103 engine-owned object, which is why it was not taken as a rider on #14456.
Not in scope of this card
failedfold, the summary line token and the$errorrow binding — that IS service-automation: populate the contained-failure visibility contract —FlowRunSummary.failedfold, loop iteration throughtry_catch→runRegion,$error.iteration/$error.item,failed=on the summary line (engine half of #13681) #14456.#13803(a summary reportingacted: 0after a mid-loop death) — independent.#15223(the run-history row foldingcancelledandtimed_outintofailedas a STATUS) — a differentfailed, onstatus, not this counter. Named here only so the two are not confused; that issue is not addressed by anything proposed above and remains open.Filed from
claude/issue-14456-contained-failure-visibility.