<!-- source-hash: 6164b984e1688be647fedf5037a794eb -->
Brief description of the file's purpose: This hook encapsulates all ticket write operations (create, update, close, reopen, reply) for the Flamingo/OpenFrame tickets UI, routing them through a single `/api/chat/agent/ticket-action` endpoint with single-flight protection, per-row mutexes, a serialization queue, and mirror-sync retry logic.

## Key Components

### `useTicketActions(options: UseTicketActionsOptions): UseTicketActionsReturn`
The primary exported hook. Accepts callback options from the parent and returns ticket action handlers plus loading state selectors.

### `UseTicketActionsOptions`
Configuration interface injected by the parent component:
- `prependOptimistic` — adds an optimistic placeholder row to the QueryClient cache before the server roundtrip
- `removeOptimistic` — removes the placeholder on mirror-sync failure or real ticket arrival
- `removeTicketFromCache` — silent cache removal on `TICKET_NOT_FOUND`
- `toast` — notification helper (injected for testability)
- `onSupportSystemDown` — called on `412 HUBSPOT_DISCONNECTED`

### `UseTicketActionsReturn`
Returned handler surface:
- `submitTicket(input)` — creates a new ticket with subject, content, and optional attachments
- `sendMessage(ticket, text, attachments)` — sends a combined reply note
- `closeTicket(ticket, resolution?)` — closes a ticket
- `reopenTicket(ticket)` — reopens a closed ticket
- `isSubmittingForm` — boolean loading state for form-level submission
- `isRowBusy(localId)` — per-row busy predicate for UI disabling

### `TicketRef`
Decouples the local mirror UUID (`id`) from the HubSpot external id (`external_id`), both required for correct mutex and server routing.

### Internal Mechanisms
| Mechanism | Purpose |
|---|---|
| `formInFlightRef` + `isSubmittingForm` | Prevents duplicate form submits across async ticks |
| `busyRowsRef` + `busyRows` | Per-row mutex preventing fan-out on the same ticket row |
| `queueRef` / `enqueue` | Depth-1 serial queue preventing server rate-limit stampedes |
| `watchMirrorSync` | 3s/6s/12s backoff refetch loop when `mirror_synced === false`; drops placeholder after 30s cap |
| `watcherControllersRef` | `AbortController` map; aborted on unmount and on duplicate-submit replacement |

## Usage Example

```typescript
const {
  submitTicket,
  sendMessage,
  closeTicket,
  reopenTicket,
  isSubmittingForm,
  isRowBusy,
} = useTicketActions({
  prependOptimistic: (placeholder) => {
    queryClient.setQueryData(['tickets'], (old: TicketData) => ({
      ...old,
      tickets: [placeholder, ...(old?.tickets ?? [])],
    }))
  },
  removeOptimistic: (id) => {
    queryClient.setQueryData(['tickets'], (old: TicketData) => ({
      ...old,
      tickets: old?.tickets.filter((t) => t.id !== id) ?? [],
    }))
  },
  removeTicketFromCache: (ticketId) => { /* cache mutation */ },
  toast,
  onSupportSystemDown: () => setSupportSystemDown(true),
})

// Create a ticket
const ok = await submitTicket({
  subject: 'VPN not connecting',
  content: 'Unable to connect since this morning.',
})

// Close a ticket
await closeTicket(
  { id: ticket.id, external_id: ticket.external_id },
  'Resolved by restarting the VPN client.'
)
```