Skip to content

fix(lark): malformed v2 card schema causes nyx-api 502, /agents silently drops reply #416

@eanzhao

Description

@eanzhao

Summary

Sending /agents to the production Lark bot results in no reply at all to the user. The backend logs the request, formats an interactive card, dispatches it to NyxID POST /api/v1/channel-relay/reply — and gets back 502 with body error code: 502 (Cloudflare's origin-error page). The reply token is single-use, so the request is marked relay_reply_token_consumedPermanentFailure and no fallback text is sent.

Other commands work:

  • daily (error path) → ✅ 200 — uses TextContent(...)
  • normal LLM chat → ✅ 200 — text streaming reply

Root cause

LarkMessageComposer.Compose() (agents/platforms/Aevatar.GAgents.Platform.Lark/LarkMessageComposer.cs:120-137) produces a hybrid v1/v2 card payload:

{
  "schema": "2.0",
  "config": { "wide_screen_mode": true },
  "header": { "title": {...}, "template": "blue" },
  "elements": [...]            ← top-level elements
}

This is not a valid Lark v2 card. Per Lark Open Platform v2 card spec, v2 cards require elements nested under body.elements:

{
  "schema": "2.0",
  "config": {...},
  "header": {...},
  "body": {
    "elements": [...]
  }
}

NyxID's lark adapter (backend/src/services/channel_adapters/lark.rs:384-396) transparently forwards metadata.card to Lark POST /open-apis/im/v1/messages as a JSON-string content field. Lark either rejects or stalls on the malformed card; NyxID's reqwest call has no explicit timeout, the origin hangs, and Cloudflare returns its standard error code: 502 page after the idle timeout.

NyxID's existing tests (lark.rs:1526-1547) only cover v1 cards (no schema field) — v2 cards from this codebase are the first ones to actually reach Lark in production.

Why this manifested only on /agents

FormatListAgentsCard always returns a MessageContent populated with Cards + Actions (interactive). All other DM commands either route through text fallbacks or LLM streaming text, so no card was actually sent until /agents.

Command Format function Payload Result
/agents FormatListAgentsCard (always cards) {reply:{metadata:{card:{schema:"2.0",elements:[...]}}}} 502
daily (error) FormatDailyReportToolReplyTextContent(...) {reply:{text:"..."}} 200
LLM chat streaming text + edits {reply:{text:"..."}} 200

Affected paths

  • agents/platforms/Aevatar.GAgents.Platform.Lark/LarkMessageComposer.cs:120-137 — primary fix site
  • agents/Aevatar.GAgents.ChannelRuntime/NyxRelayAgentBuilderFlow.cs:67,376-441/agents is the first surface to ship card payloads
  • agents/Aevatar.GAgents.ChannelRuntime/Outbound/NyxIdRelayInteractiveReplyDispatcher.cs:33-81 — wraps composer output into metadata.card

Proposed fix

Make LarkMessageComposer.Compose() emit cards that conform to Lark v2 spec. The simplest correct change: nest elements under body.elements in both the form-mode and standard branches:

var cardJson = JsonSerializer.Serialize(new
{
    schema = "2.0",
    config = new { wide_screen_mode = true },
    header = new { title = new { tag = "plain_text", content = headerTitle }, template },
    body = new { elements },           // ← was: elements (top-level)
});

Apply the same body = new { elements = formElements } change in the form branch (line 79-83).

Optionally, drop schema = "2.0" and emit a v1 card (config + header + elements at top level) — v1 is what NyxID's tests cover and is more stable, at the cost of losing v2-only features (so far we don't use any).

Acceptance

  • /agents returns a rendered Lark card listing the user's agents
  • /agent-status <id> (also card-only) returns a rendered card
  • /daily happy path still works
  • Add a unit test in LarkMessageComposerTests asserting the emitted JSON has body.elements (not top-level elements)
  • After landing, follow up with NyxID team to add v2 card schema validation + reqwest timeout (separate concern, defense in depth)

Workaround until fix lands

Route list_agents through the existing text helper:

// NyxRelayAgentBuilderFlow.cs:67
"list_agents" => TextContent(FormatListAgentsResult(doc.RootElement)),

FormatListAgentsResult already exists at the same file lines 338-367 and produces a usable plain-text listing. Loses the per-agent Status buttons but restores visibility.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions