Skip to content

fix(messaging): get_conversation HTTP 400 on all URNs + inbox/message shaping#1

Open
Dandiggas wants to merge 2 commits into
devag7:mainfrom
Dandiggas:fix/conversation-urn-and-message-shaping
Open

fix(messaging): get_conversation HTTP 400 on all URNs + inbox/message shaping#1
Dandiggas wants to merge 2 commits into
devag7:mainfrom
Dandiggas:fix/conversation-urn-and-message-shaping

Conversation

@Dandiggas

Copy link
Copy Markdown

Summary

get_conversation is currently broken for every real conversation URN (HTTP 400), and the two messaging read tools drop identity/ordering information that is already present in the raw Voyager responses. This PR fixes the URN encoding bug and upgrades the shaping so threads and messages are identifiable from the tool output alone.

1. Bug: get_conversation returns HTTP 400 for all msg_conversation URNs

Root cause. conversationMessages() builds the messaging GraphQL path with encodeURIComponent(conversationUrn). encodeURIComponent leaves ( and ) unescaped, but every msg_conversation URN embeds parentheses:

urn:li:msg_conversation:(urn:li:fsd_profile:<id>,<threadId>)

The raw parens land inside the REST-li variables=(...) expression, the server-side parser can't parse it, and Voyager answers HTTP 400 — for every conversation, always.

Repro (before this patch).

  1. get_inbox → take any conversationUrn.
  2. get_conversation with that URN → {"error":"HTTP 400 for /voyagerMessagingGraphQL/graphql?queryId=messengerMessages...","code":"HTTP_ERROR"}.

Fix. Percent-encode (%28 and )%29 explicitly after encodeURIComponent. Verified live against a real session: before 0/7 calls succeeded (100% HTTP 400) → after 5/5 succeeded, returning the full thread each time.

2. get_inbox: participant identity

The raw response already includes com.linkedin.messenger.MessagingParticipant entities referenced by each conversation's *conversationParticipants, but the shaper dropped them — and 1:1 threads have title: null — so the inbox was a list of anonymous URNs and users had to fetch every conversation to find the one they wanted.

shapeInbox now resolves participants (mailbox owner excluded, derived from the owner id embedded in the conversation URN — no extra /me call), falls back to participant names for the title, and exposes groupChat:

{
  "title": "Ada Lovelace",
  "participants": [
    {
      "name": "Ada Lovelace",
      "headline": "Engineer at Example",
      "profileUrn": "urn:li:fsd_profile:ACoAA...",
      "profileUrl": "https://www.linkedin.com/in/ACoAA..."
    }
  ],
  "groupChat": false,
  "lastActivityAt": 1700000300000,
  "unreadCount": 1,
  "read": false,
  "conversationUrn": "urn:li:msg_conversation:(urn:li:fsd_profile:ACoAA...,2-...)"
}

Verified live: all 20 inbox threads came back with resolved names in a single call.

3. get_conversation: ordering + sender attribution

Raw messages arrive in no guaranteed order and carry *sender / *actor (MessagingParticipant URNs) that the shaper ignored. shapeConversationMessages now sorts ascending by deliveredAt and attributes each message:

{
  "text": "Hi — are you free next week?",
  "deliveredAt": 1700000100000,
  "sender": "Ada Lovelace",
  "senderProfileUrn": "urn:li:fsd_profile:ACoAA...",
  "fromSelf": false
}

fromSelf is derived from the mailbox-owner id embedded in the message's *conversation URN, avoiding an extra /me round-trip. Verified live: a 20-message thread returned fully sorted and attributed, with fromSelf correct in both directions.

Tests

  • tests/endpoints.test.ts: new cases for inboxConversations() and conversationMessages() — asserts no raw parens survive in the built path and that decoding round-trips the original URN.
  • tests/normalize-messaging.test.ts (new): synthetic fixtures mirroring the live messenger GraphQL shape; covers participant resolution + owner exclusion, title fallback, ascending sort, sender attribution, fromSelf in both directions, and graceful degradation when participants are absent.

npm run typecheck, npm run lint, npm test (175 passed), and npm run build are all green.

Notes

  • Tool descriptions for get_inbox / get_conversation updated to match the new output.
  • All example URNs/names above are placeholders; live verification was done against my own account.
  • While debugging this I also hardened some lifecycle behavior locally (stale Chrome SingletonLock recovery, bounded shutdown, read-path retries/timeouts). Those are more opinionated, so I left them out of this PR — happy to follow up with a separate PR if you're interested.

🧙 Built with WOZCODE

https://claude.ai/code/session_019KMyYkdjgDzeEAjCkYMP1s

Dandiggas added 2 commits July 4, 2026 17:48
encodeURIComponent leaves ( ) unescaped, but msg_conversation URNs embed
parentheses — urn:li:msg_conversation:(urn:li:fsd_profile:<id>,<threadId>).
The raw parens break LinkedIn's REST-li variables=(...) parser, so
get_conversation returned HTTP 400 for every real conversation URN.

Encode ( and ) explicitly; add endpoint tests covering round-trip decoding
and the absence of raw parens in the built path.
…ages

get_inbox: resolve the response's MessagingParticipant entities via
*conversationParticipants and emit participants[] (name, headline,
profileUrn, profileUrl) with the mailbox owner excluded; fall back to
participant names for the title (1:1 threads have title: null) and expose
the groupChat flag. Previously threads were unidentifiable without
fetching each conversation.

get_conversation: messages arrive unordered and unattributed — sort
ascending by deliveredAt and resolve *sender/*actor to sender name +
senderProfileUrn, plus a fromSelf flag derived from the mailbox-owner id
embedded in the conversation URN (no extra /me round-trip).

Covered by tests/normalize-messaging.test.ts with synthetic fixtures.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant