{"record":{"id":"6fc019f8f43be444","repo":"sipeed/picoclaw","slug":"dingtalk-send-w","errorCode":null,"errorMessage":"dingtalk send: %w","messagePattern":"dingtalk send: %w","errorType":"exception","errorClass":"ErrTemporary","httpStatus":null,"severity":"error","filePath":"pkg/channels/dingtalk/dingtalk.go","lineNumber":271,"sourceCode":"}\n\n// SendDirectReply sends a direct reply using the session webhook\nfunc (c *DingTalkChannel) SendDirectReply(ctx context.Context, sessionWebhook, content string) error {\n\treplier := chatbot.NewChatbotReplier()\n\n\t// Convert string content to []byte for the API\n\tcontentBytes := []byte(content)\n\ttitleBytes := []byte(\"PicoClaw\")\n\n\t// Send markdown formatted reply\n\terr := replier.SimpleReplyMarkdown(\n\t\tctx,\n\t\tsessionWebhook,\n\t\ttitleBytes,\n\t\tcontentBytes,\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dingtalk send: %w\", channels.ErrTemporary)\n\t}\n\n\treturn nil\n}\n\nfunc stripLeadingAtMentions(content string) string {\n\tfields := strings.Fields(content)\n\tif len(fields) == 0 {\n\t\treturn \"\"\n\t}\n\n\ti := 0\n\tfor i < len(fields) && strings.HasPrefix(fields[i], \"@\") {\n\t\ti++\n\t}\n\tif i == 0 {\n\t\treturn strings.TrimSpace(content)\n\t}","sourceCodeStart":253,"sourceCodeEnd":289,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/dingtalk/dingtalk.go#L253-L289","documentation":"SendDirectReply's SimpleReplyMarkdown call to the session webhook failed; the error wraps channels.ErrTemporary so the manager retries with exponential backoff. Note the wrap uses only the sentinel — the underlying SDK error is discarded, so logs show 'temporary failure' without the HTTP cause.","triggerScenarios":"Replying via a stale/expired session webhook (each webhook has limited validity and call count), network failure reaching the DingTalk reply endpoint, DingTalk 5xx, or a malformed markdown body rejected by the API.","commonSituations":"Long agent processing so the reply lands after the webhook expired, server with restricted egress, DingTalk API hiccup, replying more times than the session webhook permits.","solutions":["Refresh the webhook: have the chat send a new inbound message (each callback stores a fresh SessionWebhook), then retry","Verify network egress to oapi.dingtalk.com from the host","Patch the wrap to keep the cause: fmt.Errorf(\"dingtalk send: %w: %w\", channels.ErrTemporary, err) — ErrTemporary stays matched by errors.Is while the raw error becomes visible","If it persists, log the SDK error before wrapping to diagnose expired-webhook vs network"],"exampleFix":"// before\nif err != nil {\n    return fmt.Errorf(\"dingtalk send: %w\", channels.ErrTemporary)\n}\n\n// after\nif err != nil {\n    return fmt.Errorf(\"dingtalk send: %w: %w\", channels.ErrTemporary, err)\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":"func isTemporary(err error) bool { return errors.Is(err, channels.ErrTemporary) }","tryCatchPattern":"if err := ch.Send(ctx, msg); err != nil {\n    if errors.Is(err, channels.ErrTemporary) {\n        // manager retries with backoff; on repeated failure the webhook is likely stale —\n        // prompt the chat for a new inbound message to rotate it\n        return backoff.Retry(sendOp, backoff.NewExponentialBackOff())\n    }\n    return err\n}","preventionTips":["Reply promptly after inbound messages — session webhooks expire","On repeated temporary failures, suspect a stale webhook and refresh it via a new inbound message","Patch the error wrap to include the raw SDK error for diagnosability","Keep DingTalk egress (oapi.dingtalk.com) reachable from the host"],"tags":["dingtalk","send","temporary","retry","session-webhook"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}