feat(suggested-fix): add applied_pending status for deferred verification
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s

Engineer applies a fix but can't verify yet (waiting on client power-cycle,
AD replication, async sync). Today the verifying banner forces a synchronous
verdict (worked / didn't / partial) — anything else means leaving the banner
stale or guessing wrong. This adds a fourth outcome that parks the fix in a
non-terminal "Awaiting verification" state with a reason ("waiting on what?")
and exposes it on the chat-anchored banner so the engineer doesn't lose track.

Backend
- New non-terminal status `applied_pending` parallel to `applied_partial`.
- New `pending_reason` column (nullable Text) — the "what are you waiting on?"
  prose, mirrors `partial_notes`. Required when outcome=applied_pending.
- Outcome endpoint allows pending in/out transitions; pending stamps
  applied_at but NOT verified_at (it's parked, not verified).
- Resolution-note + escalation-package prompts handle the new status:
  resolution note frames the fix as provisional; escalation package surfaces
  pending verification as the leading hypothesis with reference to what's
  being waited on.
- Migration: add column + extend status CHECK constraint.

Frontend
- New `BannerMode = 'pending'` + `PendingBanner` component (info-tone,
  parallel to PartialBanner) with worked / didn't / update-reason actions.
- VerifyingBanner overflow menu adds "Waiting to verify…".
- Nudge banner's "Still checking" button now actually records pending with
  a reason, instead of just silencing for the session.
- AssistantChatPage banner-mode derivation maps applied_pending → 'pending'.

Tests: 4 new integration tests covering pending notes requirement, reason
storage + applied_at/verified_at semantics, pending→success transition,
and pending_reason update on re-PATCH.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-30 16:28:45 -04:00
parent c0ed6d9840
commit 9eba64186c
10 changed files with 285 additions and 13 deletions

View File

@@ -20,6 +20,7 @@ FixStatus = Literal[
"applied_success",
"applied_failed",
"applied_partial",
"applied_pending",
"dismissed",
]
@@ -40,6 +41,7 @@ class SessionSuggestedFixResponse(BaseModel):
applied_at: datetime | None
verified_at: datetime | None
partial_notes: str | None
pending_reason: str | None
failure_reason: str | None
ai_outcome_proposal: dict[str, Any] | None
@@ -91,7 +93,11 @@ class SessionSuggestedFixDecisionResponse(BaseModel):
# Subset of FixStatus that the engineer can set via the outcome endpoint —
# `proposed` is excluded because you can't un-decide a fix back to "proposed".
FixOutcome = Literal[
"applied_success", "applied_failed", "applied_partial", "dismissed"
"applied_success",
"applied_failed",
"applied_partial",
"applied_pending",
"dismissed",
]
@@ -103,14 +109,18 @@ class SessionSuggestedFixOutcomeRequest(BaseModel):
engineer took); outcome captures whether the fix actually worked.
Allowed transitions:
- from `proposed` or `applied_partial`: any outcome is valid
(partial is parked, not terminal — the engineer may update notes,
abandon via dismiss, or advance to success/failed)
- from `proposed`, `applied_partial`, or `applied_pending`: any outcome
is valid. Partial means "did some of it"; pending means "did all of
it but verification is deferred (waiting on client, async sync, etc)".
Both are parked, not terminal — the engineer may advance them to
success/failed/dismiss.
- from any terminal outcome (`applied_success`, `applied_failed`,
`dismissed`): server returns 409
"""
outcome: FixOutcome
# Required for applied_partial, optional for applied_failed, ignored otherwise.
# Required for applied_partial AND applied_pending; optional for
# applied_failed; ignored otherwise. For pending, this is the
# "what are you waiting on?" reason (e.g. "client power-cycling router").
notes: str | None = Field(None, max_length=500)