Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion app/routers/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,23 @@ async def regenerate_webhook_secret_endpoint(
@router.get("/{ref}/inbox")
async def get_inbox_endpoint(
ref: str,
state: Optional[str] = Query(default=None, description="Comma-separated states; default excludes acked/expired"),
state: Optional[str] = Query(
default=None,
description=(
"Comma-separated states to include. Default excludes ``acked`` "
"+ ``expired`` (worker-runtime/agent-handler default — those "
"consumers poll for actionable messages only). "
"**Conversation/transcript consumers** (chat UI, message-thread "
"renderer, audit log, etc.) should pass an explicit "
"``state=queued,delivering,retry_ready,delivered,read,claimed,acked,expired,failed`` "
"to include the full history — otherwise messages that have "
"been ``acked`` (e.g., by the recipient's own desktop worker) "
"vanish from the conversation view while the sender's bubble "
"remains visible via the symmetric ``GET /v1/messages`` sent "
"view. See ``Conversation-view consumer note`` in the "
"docstring below."
),
),
since: Optional[datetime] = Query(default=None),
thread_id: Optional[str] = Query(default=None),
counterpart: Optional[str] = Query(
Expand Down Expand Up @@ -297,6 +313,34 @@ async def get_inbox_endpoint(
from that counterpart agent only. The atomic queued→delivered
UPDATE applies the same filter so polling a single counterpart
thread doesn't auto-deliver other threads' queued messages.

## Conversation-view consumer note (v1.1.5 docs)

The default ``state=`` filter excludes ``acked`` + ``expired``. That
default is right for **worker-runtime / agent-handler consumers**
that poll for new actionable work — they don't want to re-process
messages they've already acknowledged.

It's **wrong** for conversation/transcript consumers (chat UIs,
message-thread renderers, audit logs, support tools) because
``acked`` messages disappear from the recipient's inbox view while
they remain visible on the sender's side via ``GET /v1/messages``
(sent view). That asymmetry produces a confusing UX: the sender
sees their own message in the conversation, but the recipient's
pane shows nothing — even though the message WAS delivered and
acknowledged successfully.

**Fix for conversation views**: pass an explicit
``state=queued,delivering,retry_ready,delivered,read,claimed,acked,expired,failed``
on the inbox fetch. This includes the full state set + matches
the symmetric sent-side view, restoring conversation parity.

Empirical anchor: 2026-05-21, Dock's ``cue.dock.svc`` ``/api/threads``
consumer hit this trap — their recipient pane was empty for
cross-user messages while sent-side bubbles rendered correctly.
Substrate behavior is correct (messages WERE delivered + acked
fast by Dock's desktop worker); Dock fixed Dock-side by passing
the full state set on their threads-view fetch.
"""
result = await list_inbox(
db,
Expand Down