Skip to content

fix: contain scan matching failures to a single batch - #120

Open
mldangelo wants to merge 2 commits into
openai:mainfrom
mldangelo:fix/match-all-uncertain-collision
Open

fix: contain scan matching failures to a single batch#120
mldangelo wants to merge 2 commits into
openai:mainfrom
mldangelo:fix/match-all-uncertain-collision

Conversation

@mldangelo

@mldangelo mldangelo commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

codex-security scans match --all aborts the entire campaign when one batch produces conflicting confirmed and uncertain findings, and it does so after earlier batches have already been written.

matchAllScans throws at src/cli.ts:2006 while building comparisons inside beforeScans.map(...). That is before the failing batch's own save loop, but after every earlier iteration of the outer for (const ... of batches) loop already called save-scan-comparison. The result is a nonzero exit with partially persisted match state and no resume path — rerunning re-derives the unsaved pairs but the operator has no way to tell how far the previous run got.

What this changes

Each batch is now contained. A batch that cannot be matched is counted, warned about on stderr, and skipped; the remaining batches still save.

If nothing matched, the first failure is rethrown. This keeps a systemic problem — unwritable workbench, rejected credential — from being downgraded to a warning on an apparently successful no-op, and it preserves today's behavior exactly in the single-batch case.

The summary gains unmatchedBatches when nonzero, rendered in the TTY match-all view as N scans could not be matched; rerun to retry alongside the existing scans unavailable line.

What this deliberately does not change

The conflict check itself. Refusing to persist conflicting confirmed and uncertain findings is intentional and test-covered by tests-ts/cli-workbench.test.ts"does not save conflicting confirmed and uncertain matches". That test passes unmodified — that was the acceptance signal for this change, and I'd treat any need to edit it as a sign the fix had overreached.

Open question for maintainers (not addressed here)

There is a real disagreement between two layers that this PR leaves alone:

src/scan-comparison.ts:246-257 with allowHistoricalUncertainty: true deliberately permits an uncertain pair whose afterOccurrenceId is already claimed by a confirmed match — it only ever rejects the before-side collision via matchedBefore. The flag exists so an after-occurrence can be confirmed against one earlier scan and uncertain against another. But validateComparison has no notion of scan boundaries, so it also admits the collision within a single before-scan, which matchAllScans then treats as unusable.

That shape is not self-contradictory — "after-finding A definitely corresponds to X, and may also correspond to Y" is a coherent claim about two earlier findings merged into one — so a well-behaved model can produce it and get a skipped batch. Either scan-comparison should reject it or matchAllScans should drop the pair. I did not pick, because inverting a documented product invariant isn't mine to decide. Happy to follow up either way.

Testing / QA instructions

Baseline before this branch: 470 pass / 6 skip / 0 fail. After: 472 pass / 6 skip / 0 fail (two added tests).

Full gate, from sdk/typescript. Run pnpm from inside this directory, not the repo root — from the root, pnpm --dir resolves your global pnpm instead of the pinned packageManager and the next in-directory command tries to purge node_modules:

cd sdk/typescript
CI=true pnpm install --frozen-lockfile
CI=true pnpm run types
CI=true pnpm run test
CI=true pnpm run format
CI=true pnpm run build

The assertion that matters most — pre-existing behavior is intact

CI=true bun test --timeout 30000 ./tests-ts/cli-workbench.test.ts -t "does not save conflicting"

Expect 1 pass / 0 fail. This file is unmodified apart from added tests and one added import; if this fails, the containment scope is wrong.

New coverage

CI=true bun test --timeout 30000 ./tests-ts/cli-workbench.test.ts -t "after one batch conflicts"
CI=true bun test --timeout 30000 ./tests-ts/cli-workbench.test.ts -t "no batch could be matched"
  1. keeps matching later scans after one batch conflicts — two batches, the first conflicting. Asserts exactly one save-scan-comparison call, that it is for the healthy batch (before-ok), that stderr names the skipped scan (after-conflict), and that the JSON summary reports matchedPairs: 1, unmatchedBatches: 1. On main this exits 2 and saves nothing.
  2. reports the underlying failure when no batch could be matched — every save-scan-comparison throws a workbench-database error. Asserts exit 2 and that the underlying message reaches stderr rather than being swallowed into a zero-exit summary.

Manual check of the rendered line

The unmatchedBatches line only renders on a TTY with --format toon (default). To see it without a real workbench, call the renderer directly:

cd sdk/typescript && bun -e '
import { renderScanHistory } from "./src/scan-history-renderer.js";
console.log(renderScanHistory(
  { repository: "/repo", scanCount: 3, matchedPairs: 1, findingMatches: 1, unavailableScans: 0, unmatchedBatches: 1 },
  "match-all", { color: false }));'

Expect a 1 scans could not be matched; rerun to retry line. Confirm --format json output is unaffected (the field is simply absent when zero).

Regression surface to watch

`scans match --all` aborted the whole campaign when one batch produced
conflicting confirmed and uncertain findings. The throw fires while
building `comparisons` for a batch, which is after every earlier batch
has already called `save-scan-comparison`, so the run exited nonzero with
partially persisted match state and no way to resume.

Contain each batch instead. A batch that cannot be matched is counted,
warned about, and skipped; the remaining batches still save. When nothing
matched at all the first failure is rethrown, so an unwritable workbench
or a rejected credential still surfaces as an error rather than a warning
attached to a successful no-op.

The conflict check itself is unchanged: refusing to persist conflicting
confirmed and uncertain findings is deliberate, and its existing test
passes unmodified.
@mldangelo-oai

Copy link
Copy Markdown
Collaborator

@codex review Please review exact head 87781f3, focusing on isolating scan-matching failures to one batch, preserving unrelated comparisons, renderer behavior, and the current-main merge.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87781f3bdf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread sdk/typescript/src/cli.ts
unmatchedBatches += 1;
firstFailure ??= error;
onWarning?.(
`Could not match findings against scan ${afterScanId}: ${cliErrorMessage(error)}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Call the merged redaction helper for warnings

At the exact merged head, cliErrorMessage no longer exists because current main replaced it with the imported redactedErrorMessage. Whenever any batch fails, evaluating this warning therefore raises a ReferenceError, escapes the catch block, and prevents later healthy batches from being processed—the behavior this change is intended to preserve. Use redactedErrorMessage(error) here.

Useful? React with 👍 / 👎.

Comment thread sdk/typescript/src/cli.ts
(count, { beforeOccurrenceIds, afterOccurrenceIds }) =>
count + beforeOccurrenceIds.length * afterOccurrenceIds.length,
0,
} catch (error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not swallow persistence failures after an earlier save

This catch also contains failures from save-scan-comparison, not just matching or validation failures. If one earlier pair was saved and a later database write fails (for example because the workbench becomes unwritable), matchedPairs remains nonzero, line 2167 does not rethrow, and the command exits successfully despite a systemic persistence failure and incomplete comparisons. Restrict containment to matcher/batch-validation errors, while allowing workbench save errors to fail the command.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: 87781f3bdf

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants