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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions