fix(VIOL-0034): classify version-merge empties by cause, not file existence#771
Conversation
…stence resolve_correlated_input decided all-sources-empty from list_target_files (file existence). Two failures followed: - A guard-filtered branch that wrote an empty output file has files but zero records, so it was misread as 'has output' and fell through to a ConfigurationError the executor does not catch — the same crash this path exists to prevent. - Any all-empty state cascade-skipped and exited 0, masking sources that completed with zero output for non-filter reasons (missing data), which previously raised. Classify by cause instead: a source with output records means correlation genuinely failed (raise); no records but guard-filtered means cascade-skip; no records and not filtered means missing data (raise, naming the sources). Record counts come from the already-guarded backend loader and the filtered-disposition probe is guarded, so a storage error surfaces a clear ConfigurationError rather than a raw backend crash. Tests cover the empty-output-file skip, has-output failure, all-empty-not- filtered failure, and the guarded disposition-probe error. The correlator failure test now configures explicit output records instead of relying on incidental mock truthiness. Manifest notes the public exception.
Code reviewFound 1 issue:
agent-actions/agent_actions/workflow/managers/output.py Lines 256 to 262 in fc7f202 Context — established node-level pattern: agent-actions/agent_actions/workflow/executor.py Lines 307 to 314 in 884cf30 Context — what is actually written when all records are filtered (node-level SKIPPED, not FILTERED): agent-actions/agent_actions/processing/result_collector.py Lines 116 to 121 in 884cf30 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
…ecord filter The all-filtered probe checked has_disposition(src, filtered) with no record_id, so it returned true if any single record was filtered. A source that filtered some records then produced nothing for the rest (e.g. crashed mid-run) was read as fully filtered and cascade-skipped — the same masking this change exists to prevent. The node-level marker for 'all records filtered, no output' is a SKIPPED disposition at the node record id (written when only-guard-outcomes and no output). Probe that instead, matching how the executor checks node-level terminal dispositions. A source with per-record filters but no node marker now surfaces as missing data. Test backend now models node-level skip markers and per-record filters distinctly; adds a regression for per-record-filtered-without-node-skip.
|
Good catch — fixed in The probe was A source that filtered some records then produced nothing for the rest (crash mid-run, no node marker) now falls through to the missing-data The test backend previously keyed |
When the node-level skip probe hit a storage error it returned False, so the source fell into the missing-data branch and raised an error blaming the user's agents for a storage-layer fault. Raise a storage-specific ConfigurationError from the probe instead — still a clean, loud error rather than a raw backend crash, but one that names the actual cause.
|
Addressed the storage-error message finding in |
…ecord count The marker-based classifier reintroduced the crash it targeted. A fully guard-filtered branch writes an empty output file (record_count=0), which list_target_files returns, so the executor clears the node-level SKIPPED marker as stale. By correlation time no marker survives, so the consumer raised ConfigurationError instead of cascade-skipping — verified end to end against a real SQLite backend. Move the decision to where the truth already lives. prepare_correlated_input loads the actual version-source records; when none exist it now raises AllVersionsFilteredError (the executor cascade-skips), and a correlation or storage fault raises a clean ConfigurationError instead of collapsing every outcome to None. resolve_correlated_input delegates and propagates — the fragile marker/record re-derivation and its helpers are deleted. Classification is by record count (real data), independent of markers upstream code clears and of the storage backend. A real-backend integration test covers the empty-output-file cascade-skip, a records-present correlation, and a storage fault; correlator/handoff tests move to the raise contract.
|
The 51-agent review was right — the marker approach reintroduced the crash. Reworked in Confirmed finding #1 empirically (real SQLite backend, not mocks): a fully guard-filtered branch writes an empty output file with Root cause (finding #10): Fix: move the decision to where the truth lives. This also resolves the cluster: #2 passthrough-only (zero correlatable records → cascade-skip via record count), #3 storage faults now surface loudly and cleanly, #5 no Verification: a real-backend integration test now covers the empty-output-file cascade-skip (reproduces the crash when reverted), a records-present correlation, and a storage fault. Correlator/handoff tests moved to the raise contract. Full suite: 7848 passed; ruff + mypy clean. |
Summary
Follow-up to the merged VIOL-0034 cascade-skip (#763). A review found the emptiness discriminator in
resolve_correlated_inputkeys offlist_target_files(file existence), which mis-scopes both directions:[]) has files but zero records — so it was read as "has output" and fell through to aConfigurationErrorthe executor does not catch, re-raising the exact crash fix(VIOL-0034): cascade-skip instead of crashing when all version branches filtered #763 set out to prevent.sqlite3.Error/OSError.Fix
Classify by cause, not file existence:
ConfigurationError;has_disposition(src, filtered)) →AllVersionsFilteredError→ executor cascade-skips;ConfigurationErrornaming the sources.Record counts come from the already-guarded backend loader; the filtered-disposition probe is guarded, so a storage error surfaces a clean
ConfigurationErrorrather than a raw crash.Verification
ruff+mypyclean.AllVersionsFilteredErrorand its catch contract.Follow-ups (lower severity)
Redundant per-source re-query;
all_versions_filteredtoken vs sentence skip-reason taxonomy; shared_resolve_as_skipped(...)extraction.