Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 105 additions & 34 deletions build/api/index.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions build/api/src/domain/bugbot/context.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export interface BugbotSourceCoverage {
readonly itemsRetained: number;
readonly omittedItems: number;
readonly truncatedItems: number;
/** Exact bounded provider identities of retained conversation items truncated by the prompt. */
readonly truncatedItemIds?: readonly string[];
readonly limitReached: boolean;
/** True when a provider page cap hides an unknown number of older records. */
readonly providerLimitReached?: boolean;
Expand Down
204 changes: 151 additions & 53 deletions build/cli/index.js

Large diffs are not rendered by default.

204 changes: 151 additions & 53 deletions build/github_action/index.js

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions docs/bugbot/failure-scenarios.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ description: Diagnose terminal failures across detection, publication, autofix,
<Accordion title="Historical review cards need repair" icon="layer-group">
Bugbot updates historical review blocks in sequential batches of 20, up to 100 per run. If more remain, or a batch fails, the status card and Check report the exact unattempted count and the Action uses `workflow.presentation-pending` for a budget remainder. This does not mean the provider is unavailable or that the diff was only partly analyzed. Re-run `/copilot recheck` after inspecting any separate write failure; successful updates are kept and already-current blocks are skipped. The canonical status card remains the aggregate view throughout.
</Accordion>
<Accordion title="A native review exists but finding reconciliation failed" icon="triangle-exclamation">
Successful publication can precede a later failed marker/thread mutation. The recovery details identify the publication or resolution stage, exact operation, affected finding (plus batch count), HTTP status and provider request reference when available. Missing provider metadata is explicitly unavailable. Existing reviews, comments and durable dismissal intent are retained; a zero completed-publication count on a failed run does not prove that no native review exists.

Correct permanent credential/access or contract failures before retrying. For a transient outage or rate limit, restore provider availability first, then run `/copilot recheck` on the same PR. The normal fresh provider read reconciles existing comments and threads rather than recreating completed effects. Do not delete a review or install an application dependency merely to clear an unrelated finding-publication failure.

**Incomplete coverage** remains separate. `human-conversation` lists retained, omitted and truncated counts plus `truncated-source-ids` for exact retained records, such as `issue:6311517518` (a general issue/PR comment ID) or `review:PRRC_50` (an inline comment node ID). Inspect that source from the linked PR/issue, preserve its substantive decisions and reduce only redundant text before rechecking. Source IDs contain no comment text. A mutation retry never upgrades incomplete conversation evidence to complete.
</Accordion>
</AccordionGroup>

See [Verification commands](/bugbot/verification-commands) and [Security & Operations](/security-operations).
8 changes: 4 additions & 4 deletions specs/CATALOG.md

Large diffs are not rendered by default.

93 changes: 93 additions & 0 deletions specs/bugbot-review-state-reconciliation.md
Original file line number Diff line number Diff line change
Expand Up @@ -1601,3 +1601,96 @@ from one trusted bot-owned complete current-head card; it grants no approval
or mutation authority. Marker/reader tests (24 cases) belong exclusively to
the comment automation SDD budget. User documentation explains recheck recovery
when evidence is absent or stale.


### Actionable finding mutation failures (2026-10-21, #646)

Observed: a native review and inline comment could persist before resolution
failed; the adapter and application discarded the failed operation/HTTP facts,
leaving only `provider.unavailable`. The exact consumer API cause remains unknown.
This amendment MUST preserve incomplete evidence and completed effects while
retaining closed stage (`publication`/`resolution`), adapter operation, first
finding ID, affected batch count, optional HTTP status and safe provider request
reference. Batch diagnosis names its first finding and count, without falsely
claiming which individual item failed inside an atomic provider request.

Only integer HTTP 400–599 and a 1–64-character alphanumeric/colon/hyphen request
reference are retained. Raw error prose, response bodies, URLs, request headers,
credentials and rejected provider content MUST NOT cross this diagnostic boundary.
Finding IDs use a bounded 200-character identifier alphabet; unavailable or unsafe
IDs are represented explicitly. Existing locally computed fingerprints remain
reconciliation authority; diagnostic IDs grant no mutation permission.

```text
Publish native review -> attempt durable reconciliation -> failure retains effects
-> safe diagnostic plus independent coverage -> correct cause -> normal recheck
-> fresh provider read -> reuse native finding and retained dismissal
```

Text equivalent: partial success is retained and diagnosed; a recheck discovers
existing provider state before continuing, without duplicate review/thread creation
or reversal of a durable human dismissal. There is no new automatic retry or
rollback and no change to freshness, ownership, evidence or mutation ordering.

The semantic error class MUST preserve authorization, not-found, conflict,
rate-limit, client-contract and transient-provider categories. Recovery details
in the owned status card remain localized with existing English/Spanish catalogs.
Example: “Finding mutation failed: stage=resolution; operation=resolve-thread;
finding=backend/Dockerfile:20:healthcheck-requires-curl; affected=1; http-503;
provider reference=AB12:34CD. Completed effects are retained.” Correct access and
contract failures before retrying; restore availability for transient failures,
then recheck the same PR. Existing pending, attention and successful cards remain.
No new comment stream, permission, configuration input or durable marker schema.
Absent metadata remains unavailable; no inference of a specific provider cause.

Conversation truncation MUST remain independently partial. Coverage can attach
up to 50 bounded provider identities for retained truncated sources (`issue:<id>`
or `review:<node-id>`), never comment text or arbitrary links. The aggregate shows
these identities under Incomplete coverage, separately from Recovery details.
A failed mutation cannot turn partial coverage into complete, and a coverage
limitation cannot conceal the semantic mutation failure. Existing projection
precedence (partial coverage may yield a partial projection with failed Result)
is retained. Old coverage without identities remains compatible and unverified.

Adapters own safe provider metadata; application errors own the credential-free
mutation diagnostic; publication/resolution use cases bind finding identity;
reconciliation and catalog presentation consume typed facts. Domain coverage
adds optional source identities only. Typecheck, architecture, localization and
fault-injection regressions enforce these boundaries; SDKs remain outside policy.

The additional budget is **26 distinct cases**: thirteen HTTP/classification/privacy
and error-preservation cases, five publication-stage identity/batch cases, one real
publication-to-failed-resolution-to-retry integration, two locale/projection and
independent-coverage cases, two exact-source/privacy cases and three adapter cases.
Existing global coverage/quality thresholds and all prior state/replay tests
remain required. Acceptance proves a persisted native review survives a subsequent
503, one recheck produces no duplicate native review/thread, dismissal bytes are
retained, and diagnostics distinguish that error from truncated conversation.
Documentation: Bugbot failure scenarios. Upgrade the Action and recheck the same
PR after recovery; rollback only loses richer diagnostics. Definition of Done:
all package, documentation, catalog and existing architecture gates pass; no live
consumer recovery is claimed by deterministic fixtures.


PR #651 review correction: batch mutation diagnostics MUST derive identity and
count from the publisher's actually queued new findings, excluding individually
updated or suppressed findings. Overflow summaries add their represented overflow
count; an overflow-only batch reports that count and an unavailable finding ID,
never a fabricated durable identity. The mixed/overflow cases above enforce this.


PR #651 status-classification correction: HTTP 408 and 425 MUST retain transient
`provider.unavailable` semantics rather than fall through to client-contract
failure. These are two additional cases in the budget above.
[RFC 9110 §15.5.9](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.9)
describes retry of an outstanding request after 408;
[RFC 8470 §5.2](https://www.rfc-editor.org/rfc/rfc8470.html#section-5.2)
requires a retry after 425 to avoid early data. Copilot adds no automatic replay: a
normal recheck obtains fresh provider state before continuing, preserving completed
effects and dismissal intent. Other client statuses retain their explicit mapping.


PR #651 issue-route correction: only PR review publication enters the new typed
review-mutation diagnostic. Issue-comment publication retains its existing
application normalization path and MUST NOT be relabeled as `publish-comments`
or a PR recheck target. A distinct issue-path regression enforces this boundary.
10 changes: 7 additions & 3 deletions specs/catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@
"status": "implemented",
"scope": "Reconcile review snapshots, findings, threads, comments, and check conclusions",
"owner": "Copilot maintainers",
"lastVerified": "2026-10-11",
"lastVerified": "2026-10-21",
"specs": [
"specs/bugbot-review-state-reconciliation.md"
],
Expand Down Expand Up @@ -476,7 +476,9 @@
"src/application/usecases/steps/commit/bugbot/prepare_bugbot_findings_policy.ts",
"src/application/usecases/steps/commit/bugbot/publish_pr_review_comments.ts",
"src/application/usecases/steps/commit/bugbot/synchronize_bugbot_review_presentation_use_case.ts",
"src/application/policies/bugbot_status_count_marker_policy.ts"
"src/application/policies/bugbot_status_count_marker_policy.ts",
"src/application/errors/bugbot_mutation_error.ts",
"src/data/repository/pull_request/pull_request_review_error_metadata.ts"
],
"tests": [
"src/domain/bugbot/__tests__/review_state.test.ts",
Expand All @@ -500,7 +502,9 @@
"src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts",
"src/application/usecases/steps/commit/__tests__/detect_potential_problems_use_case.test.ts",
"src/infrastructure/composition/__tests__/pull_request_use_case_composition_root.test.ts",
"src/tooling/__tests__/validate_workflow_contract.test.ts"
"src/tooling/__tests__/validate_workflow_contract.test.ts",
"src/application/errors/__tests__/bugbot_mutation_error.test.ts",
"src/application/usecases/steps/commit/bugbot/__tests__/finding_mutation_recovery.test.ts"
],
"documentation": [
"docs/bugbot/detection.mdx",
Expand Down
2 changes: 2 additions & 0 deletions src/application/contracts/bugbot_reconciliation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { BugbotMutationDiagnostic } from '../errors/bugbot_mutation_error';
import type { BugbotIssueComment } from '../ports/bugbot_issue_read_ports';
import type { BugbotReviewNavigation } from '../ports/bugbot_review_navigation_ports';
import type {
Expand Down Expand Up @@ -60,6 +61,7 @@ export type BugbotReconciliationSnapshotResult =
};

export type BugbotPresentationDiagnostic =
| { readonly code: 'finding-mutation-failed'; readonly mutation: BugbotMutationDiagnostic }
| { readonly code: 'operation-failed'; readonly operatorMessage: string }
| { readonly code: 'identity-unavailable' }
| { readonly code: 'snapshot-pull-request-comments-failed' }
Expand Down
36 changes: 36 additions & 0 deletions src/application/errors/__tests__/bugbot_mutation_error.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { BugbotMutationError, bugbotMutationError } from '../bugbot_mutation_error';
import { PullRequestReviewOperationError, toPullRequestReviewOperationError } from '../../ports/pull_request_review_errors';

it.each([
[401, 'authorization.credential-invalid'], [403, 'authorization.denied'], [404, 'provider.not-found'],
[409, 'provider.conflict'], [422, 'provider.contract-invalid'], [429, 'provider.rate-limited'],
[408, 'provider.unavailable'], [425, 'provider.unavailable'],
[503, 'provider.unavailable'],
])('preserves safe HTTP %s metadata and classifies recovery as %s', (status, code) => {
const cause = new PullRequestReviewOperationError('resolve-thread', { providerStatus: status, providerReference: 'AB12:34CD' });
const error = bugbotMutationError(cause, 'resolution', ['backend/Dockerfile:20:healthcheck-requires-curl'], 'mark-resolved');
expect(error).toMatchObject({ code, diagnostic: { stage: 'resolution', operation: 'resolve-thread',
providerStatus: `http-${status}`, providerReference: 'AB12:34CD' } });
expect(JSON.stringify(error)).not.toMatch(/private-/);
if (status === 408 || status === 425) expect(error.retryable).toBe(true);
});

it('drops malformed provider status/reference and unsafe finding markup', () => {
const cause = new PullRequestReviewOperationError('publish-comments', { providerStatus: 400.5, providerReference: '@team\nsecret' });
const error = bugbotMutationError(cause, 'publication', ['@team<script>'], 'publish-comments');
expect(error.diagnostic).toMatchObject({ findingId: 'unavailable', providerStatus: 'http-unavailable', providerReference: 'unavailable' });
});

it('retains a typed mutation error unchanged across application boundaries', () => {
const error = bugbotMutationError(new Error('private'), 'publication', [], 'publish-comments');
expect(error).toBeInstanceOf(BugbotMutationError);
expect(bugbotMutationError(error, 'resolution', ['other'], 'mark-resolved')).toBe(error);
expect(JSON.stringify(error)).not.toContain('private');
});


it.each([null, 'private provider prose'])('does not retain non-object provider errors', cause => {
const error = bugbotMutationError(toPullRequestReviewOperationError(cause, 'update-comment'), 'publication', ['f1'], 'publish-comments');
expect(error.diagnostic.providerStatus).toBe('http-unavailable');
expect(error.message).not.toContain('private');
});
37 changes: 37 additions & 0 deletions src/application/errors/bugbot_mutation_error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ApplicationError, type ApplicationErrorCode } from './application_error';
import { PullRequestReviewOperationError, type PullRequestReviewOperation } from '../ports/pull_request_review_errors';

export interface BugbotMutationDiagnostic {
readonly stage: 'publication' | 'resolution';
readonly operation: PullRequestReviewOperation;
readonly findingId: string;
readonly affectedCount: number;
readonly providerStatus: string;
readonly providerReference: string;
readonly errorCode: ApplicationErrorCode;
}

export class BugbotMutationError extends ApplicationError {
constructor(readonly diagnostic: BugbotMutationDiagnostic) {
super(diagnostic.errorCode, `Bugbot ${diagnostic.stage} failed: operation=${diagnostic.operation}; finding=${diagnostic.findingId}; affected=${diagnostic.affectedCount}; ${diagnostic.providerStatus}; provider-reference=${diagnostic.providerReference}. Completed effects are retained; correct the named cause and recheck the same target.`);
}
}

export function bugbotMutationError(cause: unknown, stage: BugbotMutationDiagnostic['stage'], findingIds: readonly string[], fallback: PullRequestReviewOperation, affectedCount = findingIds.length): BugbotMutationError {
if (cause instanceof BugbotMutationError) return cause;
const provider = cause instanceof PullRequestReviewOperationError ? cause : undefined;
const status = provider?.providerStatus;
const errorCode = status === 401 ? 'authorization.credential-invalid' : status === 403 ? 'authorization.denied'
: status === 404 ? 'provider.not-found' : status === 409 ? 'provider.conflict'
: status === 429 ? 'provider.rate-limited'
: status === 408 || status === 425 ? 'provider.unavailable'
: status && status < 500 ? 'provider.contract-invalid' : 'provider.unavailable';
const first = findingIds[0];
return new BugbotMutationError(Object.freeze({
stage, operation: provider?.operation ?? fallback,
Comment thread
vypbot marked this conversation as resolved.
findingId: typeof first === 'string' && /^[A-Za-z0-9_./:-]{1,200}$/.test(first) ? first : 'unavailable',
affectedCount: Math.min(affectedCount, 10_000),
providerStatus: status ? `http-${status}` : 'http-unavailable',
providerReference: provider?.providerReference ?? 'unavailable', errorCode,
}));
}
4 changes: 4 additions & 0 deletions src/application/policies/bugbot_message_catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const BUGBOT_MESSAGE_IDS = Object.freeze([
'bugbot.overflow.body',
'bugbot.common.more',
'bugbot.diagnostic.operationFailed',
'bugbot.diagnostic.findingMutationFailed',
'bugbot.diagnostic.identityUnavailable',
'bugbot.diagnostic.pullRequestCommentsFailed',
'bugbot.diagnostic.reviewThreadsFailed',
Expand Down Expand Up @@ -173,6 +174,7 @@ const ENGLISH_MESSAGES: Readonly<Record<BugbotMessageId, CatalogMessage>> = Obje
'bugbot.overflow.heading': 'More findings (comment limit)',
'bugbot.overflow.body': Object.freeze({ one: 'There is {count} more finding that was not published as an individual comment. Review locally or in the full diff to see the list.', other: 'There are {count} more findings that were not published as individual comments. Review locally or in the full diff to see the list.' }),
'bugbot.common.more': Object.freeze({ one: 'and {count} more.', other: 'and {count} more.' }),
'bugbot.diagnostic.findingMutationFailed': 'Finding mutation failed: stage={stage}; operation={operation}; finding={findingId}; affected={affectedCount}; {providerStatus}; provider reference={providerReference}. Completed effects are retained. Correct access/contract errors before retrying; for transient failures restore provider availability, then recheck the same PR. Recheck reconciles existing reviews and threads.',
'bugbot.diagnostic.operationFailed': 'Bugbot could not complete one or more finding mutations.',
'bugbot.diagnostic.identityUnavailable': 'The authenticated Bugbot identity is unavailable.',
'bugbot.diagnostic.pullRequestCommentsFailed': 'Unable to re-read pull request review comments.',
Expand Down Expand Up @@ -263,6 +265,7 @@ const SPANISH_MESSAGES: Readonly<Record<BugbotMessageId, CatalogMessage>> = Obje
'bugbot.overflow.heading': 'Más hallazgos (límite de comentarios)',
'bugbot.overflow.body': Object.freeze({ one: 'Hay {count} hallazgo más que no se publicó como comentario individual. Revísalo localmente o en el diff completo para consultar la lista.', many: 'Hay {count} hallazgos más que no se publicaron como comentarios individuales. Revísalos localmente o en el diff completo para consultar la lista.', other: 'Hay {count} hallazgos más que no se publicaron como comentarios individuales. Revísalos localmente o en el diff completo para consultar la lista.' }),
'bugbot.common.more': Object.freeze({ one: 'y {count} más.', many: 'y {count} más.', other: 'y {count} más.' }),
'bugbot.diagnostic.findingMutationFailed': 'Falló una mutación: fase={stage}; operación={operation}; hallazgo={findingId}; afectados={affectedCount}; {providerStatus}; referencia del proveedor={providerReference}. Se conservan los efectos completados. Corrige acceso/contrato antes de reintentar; para fallos transitorios restaura el proveedor y revisa la misma PR. La revisión reconcilia comentarios e hilos existentes.',
'bugbot.diagnostic.operationFailed': 'Bugbot no pudo completar una o más mutaciones de hallazgos.',
'bugbot.diagnostic.identityUnavailable': 'La identidad autenticada de Bugbot no está disponible.',
'bugbot.diagnostic.pullRequestCommentsFailed': 'No se pudieron volver a leer los comentarios del review del pull request.',
Expand Down Expand Up @@ -326,6 +329,7 @@ export function renderBugbotDiagnostic(
catalog: BugbotMessageCatalog,
): string {
switch (diagnostic.code) {
case 'finding-mutation-failed': return catalog.message('bugbot.diagnostic.findingMutationFailed', { ...diagnostic.mutation });
case 'operation-failed': return catalog.message('bugbot.diagnostic.operationFailed');
case 'identity-unavailable': return catalog.message('bugbot.diagnostic.identityUnavailable');
case 'snapshot-pull-request-comments-failed': return catalog.message('bugbot.diagnostic.pullRequestCommentsFailed');
Expand Down
Loading
Loading