Skip to main content

Expert Queue β€” Item States

Reference: states for genuine Expert work only. Ordinary successful Hermes turns never enter this state machine. Last updated: 2026-06-02 Audience: Experts, Engineering, AM, QA Surface: /workspace/queue (Expert workspace) Related: Expert Queue feature, Conversation Status Lifecycle, ADR-007 Expert Access Scope


TL;DR β€” the one question that distinguishes them​

Every state answers "who is being waited on right now?"

UI labelDB valueWho's it waiting on?Terminal?
PendingpendingThe Expert handling a real request/failure/correctionNo
Awaitingawaiting_clientThe client (their reply)No
ResolvedresolvedNobody β€” closedYes
DeferredsnoozedNobody for now β€” snoozedNo

One-liner: Pending = your turn Β· Awaiting = client's turn Β· Resolved = done Β· Deferred = look at it later.


State machine​

explicit human request / genuine failure / intentional Expert open / defer expiry
β”‚
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Pending β”‚ ← waiting on Expert
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
Expert replies Defer Resolve
β”‚ β”‚ β”‚
β–Ό β–Ό β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Awaiting β”‚ β”‚ Deferred β”‚ β”‚ Resolved β”‚
β”‚ (client) β”‚ β”‚ (snoozed)β”‚ β”‚ (final) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
defer TTL expires
(cron, automatic)
β”‚
└──► Pending

A normal client reply is processed by Hermes and does not transition this state machine.


1. Pending β€” pending​

Meaning: the ball is in the Expert's court for genuine human work. It is never created merely because Hermes produced a successful reply.

Entry triggers:

  1. The client explicitly requests a human, or the managed turn genuinely fails
  2. An authorized Expert intentionally opens the conversation for correction/follow-up
  3. A deferred item's timer expires β†’ cron resurfaces that existing human-owned item

Transitions out:

  • β†’ Awaiting when the Expert sends a reply (POST /expert-queue/:id/respond)
  • β†’ Resolved when the Expert resolves it (POST /expert-queue/:id/resolve)
  • β†’ Deferred when the Expert defers it (POST /expert-queue/:id/defer)

2. Awaiting β€” awaiting_client​

Meaning: the ball is in the client's court. The Expert has already replied and is waiting for the client to respond. (UI may label this "Awaiting Reply".)

Entry trigger: an Expert sends a reply via POST /expert-queue/:id/respond. The atomic claim flips the item to awaiting_client and updates the underlying conversation.

Key constraint: an Expert cannot respond again to an awaiting_client item. They must wait for the client to re-trigger it back to pending (a force=true path is planned but not implemented). This prevents double-sends.

Transitions out:

  • A normal client reply starts a normal managed Hermes turn; it does not fabricate or reactivate Expert work
  • β†’ Resolved / Deferred via explicit Expert action

3. Resolved β€” resolved​

Meaning: the conversation is closed. The Expert has taken final action; no further work is expected. This is a terminal state.

Entry trigger: an Expert explicitly resolves via POST /expert-queue/:id/resolve (or PATCH /expert-queue/:id/status). Stamps resolvedBy + resolvedAt.

Terminal guard: responding to a resolved item returns 409 Conflict β€” re-opening it would overwrite the resolved state and lose the audit trail. Only an admin can manually move it back via PATCH /status (rare).

Transitions out: none automatic (terminal).


4. Deferred β€” snoozed​

Meaning: temporarily set aside. The Expert wants to revisit it later, so it's hidden from the active queue until the snooze timer expires.

⚠️ Naming mismatch β€” read this. The UI says Deferred, the action endpoint is /defer, but the DB value is snoozed. When inspecting logs or the database, snoozed == the UI's "Deferred".

Entry trigger: Expert calls POST /expert-queue/:id/defer with optional deferHours (default 24h). Sets deferredUntil and removes it from the active list.

Auto-resurfacing: a BullMQ scheduled job (snooze-resurface) periodically finds items whose deferredUntil < now() and flips them back to Pending.

Transitions out:

  • β†’ Previous status when the Expert manually reverts (POST /expert-queue/:id/revert). The status held immediately before deferral is captured in previous_status at defer time and restored exactly β€” a deferred awaiting_client item reverts to awaiting_client, not pending. Falls back to Pending when previous_status is null (items deferred before the column existed). This is the only manual status change the Expert workspace UI offers (#2700).
  • β†’ Pending automatically when the defer TTL expires (cron). Auto-expiry intentionally lands on Pending regardless of previous_status β€” after the snooze window the item resurfaces for Expert attention.

Manual status changes & the PATCH /status override (#2700)​

The Expert workspace deliberately exposes no free-form status picker. Every transition happens through a dedicated, state-machine-validated action:

To change…UseAllowed from
β†’ AwaitingPOST /respondPending (automatic on Expert reply)
β†’ ResolvedPOST /resolve or PATCH /status (status=resolved)Pending / Awaiting
β†’ DeferredPOST /deferPending / Awaiting
Deferred β†’ previousPOST /revertSnoozed only

PATCH /expert-queue/:id/status validates the transition server-side: a non-admin caller may only move an item β†’ resolved (the Resolve button). Invalid manual transitions β€” pending ↔ awaiting_client, anything out of resolved (terminal), or out of snoozed (use /revert) β€” return 409 Conflict. SuperAdmin bypasses the validation for the rare documented override (e.g. reopening a resolved item). Before #2700 the UI offered a dropdown that allowed all of these; the dropdown was removed and the server now enforces the rules regardless of client.


Naming reference (UI ↔ endpoint ↔ DB)​

UI labelAction endpointDB value
Pending(default on create)pending
AwaitingPOST /respondawaiting_client
ResolvedPOST /resolveresolved
DeferredPOST /defersnoozed

Key endpoints​

EndpointEffect
GET /expert-queue?status=pending|awaiting_client|resolved|snoozed|allList by status
POST /expert-queue/:id/respondPending β†’ Awaiting
POST /expert-queue/:id/resolvePending/Awaiting β†’ Resolved
POST /expert-queue/:id/defer→ Deferred (snoozed), default 24h; captures previous_status
POST /expert-queue/:id/revertDeferred β†’ previous status (manual un-defer); 409 if not snoozed
PATCH /expert-queue/:id/statusValidated: non-admin β†’ resolved only; SuperAdmin override bypasses (see #2700 above)

Source of truth​

  • Entity + status column: api/src/common/entities.ts (ExpertQueueItem)
  • State transitions: api/src/expert-queue/expert-queue.service.ts
  • Client-reply auto-reopen: api/src/conversations/conversations.service.ts
  • UI labels: frontend/src/components/ui/TicketStatusBadge.tsx, frontend/src/app/workspace/queue/