feat(pilot): Phase 4 — Resolve + Escalate PSA writebacks with status verification
All checks were successful
Mirror to GitHub / mirror (push) Successful in 11s
All checks were successful
Mirror to GitHub / mirror (push) Successful in 11s
Wires the preview popover's Confirm & post action to ConnectWise (and,
via the provider pattern, any future PSA). Adds the parallel Escalate
flow with the handoff-oriented five-section markdown. Sessions without a
linked PSA ticket resolve/escalate locally — markdown stored, status
flipped, nothing posted externally.
Backend:
- EscalationPackageGeneratorService: Sonnet, five sections (Problem /
What we've confirmed / What we've tried / Current hypothesis /
Suggested next steps). Shares the preview_cache with a separate KIND
so Resolve and Escalate previews for the same state coexist.
- PSAWritebackService: post_resolution_note (RESOLUTION note type,
customer-visible), post_escalation_package (INTERNAL_ANALYSIS,
handoff for the next engineer only), transition_ticket_status with
mandatory re-fetch verification. PSAStatusVerificationError surfaces
loudly when CW silently rejects a status change — the
ConnectWise anti-pattern CLAUDE.md flags.
- Endpoints:
* POST /ai-sessions/{id}/escalation-package/preview
* POST /ai-sessions/{id}/resolution-note/post
* POST /ai-sessions/{id}/escalation-package/post
Outcomes: "resolved" / "escalated" with external_id + verified status,
"resolved_local" / "escalated_local" when no PSA linked.
- Target CW status IDs live in account_settings.preferences
(cw_resolved_status_id, cw_escalated_status_id). When unset, the post
proceeds without a status transition — response includes a
status_transition_skipped_reason rather than silently erroring.
- 7 tests: local-only path, PSA happy path with verified transition,
status verification failure → 502, skipped transition when
unconfigured, 409 on already-resolved re-post, escalate parallel path,
internal-analysis note type enforced.
Frontend:
- ResolutionNotePreview now kind-parameterized ('resolve' | 'escalate')
with inline edit + Confirm & post. Preview loads from the matching
backend endpoint; posting calls the matching endpoint; outcome toast
surfaces the verified CW status or the local-only result.
- AssistantChatPage: previewKind state replaces previewOpen; two toggle
buttons (Preview Resolve note / Escalate instead) in the lane's bottom
slot. handleConfirmPost dispatches by kind.
Verified 2026-04-22:
- Local-only Resolve + Escalate round-trip against the dev stack.
- Live Sonnet escalation-package preview; cache hit on repeat call
with no state change (separate cache kind from resolution-note).
- PSA post + status-verification paths covered by mocked-provider pytest
cases. Live CW round-trip pending a test CW instance.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,16 @@ export interface ResolutionNotePreview {
|
||||
from_cache: boolean
|
||||
}
|
||||
|
||||
export interface ResolutionPostResponse {
|
||||
outcome: 'resolved' | 'escalated' | 'resolved_local' | 'escalated_local'
|
||||
session_status: string
|
||||
external_id: string | null
|
||||
posted_at: string | null
|
||||
verified_status_id: number | null
|
||||
verified_status_name: string | null
|
||||
status_transition_skipped_reason: string | null
|
||||
}
|
||||
|
||||
export const sessionSuggestedFixesApi = {
|
||||
/**
|
||||
* Returns the active suggested fix for a session, or `null` if there isn't one.
|
||||
@@ -79,6 +89,45 @@ export const sessionSuggestedFixesApi = {
|
||||
)
|
||||
return r.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Parallel to getResolutionNotePreview, for the Escalate flow (five-section
|
||||
* handoff markdown). Separate backend cache kind, same state_version invariant.
|
||||
*/
|
||||
async getEscalationPackagePreview(sessionId: string): Promise<ResolutionNotePreview> {
|
||||
const r = await apiClient.post<ResolutionNotePreview>(
|
||||
`/ai-sessions/${sessionId}/escalation-package/preview`,
|
||||
)
|
||||
return r.data
|
||||
},
|
||||
|
||||
/**
|
||||
* Post the engineer-edited resolution markdown to PSA + mark session resolved.
|
||||
* Local-only when the session has no linked PSA ticket (outcome = "resolved_local").
|
||||
*/
|
||||
async postResolutionNote(
|
||||
sessionId: string,
|
||||
markdown: string,
|
||||
resolutionSummary?: string,
|
||||
): Promise<ResolutionPostResponse> {
|
||||
const r = await apiClient.post<ResolutionPostResponse>(
|
||||
`/ai-sessions/${sessionId}/resolution-note/post`,
|
||||
{ markdown, resolution_summary: resolutionSummary },
|
||||
)
|
||||
return r.data
|
||||
},
|
||||
|
||||
async postEscalationPackage(
|
||||
sessionId: string,
|
||||
markdown: string,
|
||||
escalationReason?: string,
|
||||
): Promise<ResolutionPostResponse> {
|
||||
const r = await apiClient.post<ResolutionPostResponse>(
|
||||
`/ai-sessions/${sessionId}/escalation-package/post`,
|
||||
{ markdown, escalation_reason: escalationReason },
|
||||
)
|
||||
return r.data
|
||||
},
|
||||
}
|
||||
|
||||
export default sessionSuggestedFixesApi
|
||||
|
||||
Reference in New Issue
Block a user