# Phase 9 Review Issues

Date: 2026-04-24

Scope reviewed:
- `backend/app/api/endpoints/script_builder.py`
- `backend/app/api/endpoints/session_suggested_fixes.py`
- `backend/app/services/script_builder_service.py`
- `frontend/src/pages/AssistantChatPage.tsx`
- `frontend/src/components/pilot/ScriptBuilderTab.tsx`
- `frontend/src/components/pilot/EscalateInterceptDialog.tsx`

## 1. "Applied partially" from the escalation intercept cannot persist

Severity: High

The escalation intercept offers an "applied partially" choice, but the frontend
sends `applied_partial` without notes. The backend requires notes for that
outcome and returns 400. The frontend catches the error silently and still opens
the conclude modal, so the user can believe the partial outcome was recorded
when it was not.

Relevant files:
- `frontend/src/pages/AssistantChatPage.tsx:659`
- `frontend/src/components/pilot/EscalateInterceptDialog.tsx:56`
- `backend/app/api/endpoints/session_suggested_fixes.py:316`

Why this matters:
- `handleInterceptChoice()` maps the partial button directly to
  `patchOutcome(..., "applied_partial")`.
- The call does not provide `notes`.
- `PATCH /suggested-fixes/{fix_id}/outcome` rejects `applied_partial` without
  notes.
- The catch block is silent and the UI continues into the conclude flow.
- The recorded fix status therefore remains unchanged while the user sees a
  flow that implies the partial outcome was accepted.

Recommended fix:
- Prompt for partial notes before calling `patchOutcome()` with
  `applied_partial`.
- Do not proceed to the conclude modal if the partial outcome write fails.
- Consider hiding or disabling the partial option when it is not applicable, or
  pass the current fix status into `EscalateInterceptDialog` so it can render
  valid choices only.
- Add a regression test covering the partial escalation-intercept path.

## 2. Script Builder can attach stale script state to a newer active fix

Severity: Medium/High

`ScriptBuilderTab` keeps local builder state across active-fix changes within
the same pilot chat. If a new active fix supersedes the previous one while the
tab remains mounted, old messages, `latestScript`, or editor text can remain in
memory while submission uses the new `fix.id`.

Relevant files:
- `frontend/src/components/pilot/ScriptBuilderTab.tsx:55`
- `frontend/src/components/pilot/ScriptBuilderTab.tsx:78`
- `frontend/src/components/pilot/ScriptBuilderTab.tsx:150`
- `frontend/src/pages/AssistantChatPage.tsx:399`
- `frontend/src/pages/AssistantChatPage.tsx:1630`

Why this matters:
- `ScriptBuilderTab` initializes `editorBuffer`, messages, and latest script
  from props and builder-session data.
- The create/resume effect depends on `pilotSessionId`, not `fix.id`.
- `AssistantChatPage` detects active-fix changes but only closes the script
  panel.
- The rendered `ScriptBuilderTab` is not keyed by active fix id.
- Submitting a stale builder draft calls the script patch endpoint with the
  current `fix.id`, so an older script can be attached to a newer fix.

Recommended fix:
- Reset Script Builder local state when `activeFix.id` changes.
- Key the rendered `ScriptBuilderTab` by `activeFix.id` if the intended UX is a
  fresh builder surface per fix.
- If inline builder conversations are intended to resume per fix, extend the
  backend idempotency model to include the fix id instead of only
  `(user_id, ai_session_id)`.
- Add a frontend regression test for an active fix changing while the Script
  Builder tab is mounted.

## Review Context

This review was based on code inspection of the latest committed Phase 9
implementation. No tracked working-tree diffs were present at review time.

