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_consumed → PermanentFailure 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) |
FormatDailyReportToolReply → TextContent(...) |
{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
Summary
Sending
/agentsto 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 NyxIDPOST /api/v1/channel-relay/reply— and gets back 502 with bodyerror code: 502(Cloudflare's origin-error page). The reply token is single-use, so the request is markedrelay_reply_token_consumed→PermanentFailureand no fallback text is sent.Other commands work:
daily(error path) → ✅ 200 — usesTextContent(...)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
elementsnested underbody.elements:{ "schema": "2.0", "config": {...}, "header": {...}, "body": { "elements": [...] } }NyxID's lark adapter (
backend/src/services/channel_adapters/lark.rs:384-396) transparently forwardsmetadata.cardto LarkPOST /open-apis/im/v1/messagesas a JSON-stringcontentfield. Lark either rejects or stalls on the malformed card; NyxID's reqwest call has no explicit timeout, the origin hangs, and Cloudflare returns its standarderror code: 502page after the idle timeout.NyxID's existing tests (
lark.rs:1526-1547) only cover v1 cards (noschemafield) — v2 cards from this codebase are the first ones to actually reach Lark in production.Why this manifested only on
/agentsFormatListAgentsCardalways returns aMessageContentpopulated withCards+Actions(interactive). All other DM commands either route through text fallbacks or LLM streaming text, so no card was actually sent until/agents./agentsFormatListAgentsCard(always cards){reply:{metadata:{card:{schema:"2.0",elements:[...]}}}}daily(error)FormatDailyReportToolReply→TextContent(...){reply:{text:"..."}}{reply:{text:"..."}}Affected paths
agents/platforms/Aevatar.GAgents.Platform.Lark/LarkMessageComposer.cs:120-137— primary fix siteagents/Aevatar.GAgents.ChannelRuntime/NyxRelayAgentBuilderFlow.cs:67,376-441—/agentsis the first surface to ship card payloadsagents/Aevatar.GAgents.ChannelRuntime/Outbound/NyxIdRelayInteractiveReplyDispatcher.cs:33-81— wraps composer output intometadata.cardProposed fix
Make
LarkMessageComposer.Compose()emit cards that conform to Lark v2 spec. The simplest correct change: nestelementsunderbody.elementsin both the form-mode and standard branches: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 + elementsat 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
/agentsreturns a rendered Lark card listing the user's agents/agent-status <id>(also card-only) returns a rendered card/dailyhappy path still worksLarkMessageComposerTestsasserting the emitted JSON hasbody.elements(not top-levelelements)Workaround until fix lands
Route
list_agentsthrough the existing text helper:FormatListAgentsResultalready 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