Files
openclaw/extensions/line/src/send-retry.ts
Eden 12138d2cee fix(line): retry lost pushes without duplicating an accepted send (#124464)
A LINE push made exactly one attempt, so a transient provider or transport
failure dropped the reply even though retrying was safe to do. Retrying alone
would have duplicated a send LINE already accepted, so every push now carries an
X-Line-Retry-Key and reuses it across attempts: LINE answers a replayed key with
409 and the accepted request's sent messages, which resolves to the original
delivery instead of a second message.

Retries follow LINE's documented policy - server errors and transport failures
only, never 2xx, 409 or any 4xx - and run through the shared channel API retry
runner in strict mode. Replies stay single-attempt because LINE offers no retry
key for them.
2026-08-16 15:21:20 -04:00

38 lines
1.4 KiB
TypeScript

// Line plugin module implements push retry policy behavior.
import { HTTPFetchError } from "@line/bot-sdk";
import { collectErrorGraphCandidates, extractErrorCode } from "openclaw/plugin-sdk/error-runtime";
import {
classifyTransientNetworkErrorCode,
createChannelApiRetryRunner,
} from "openclaw/plugin-sdk/retry-runtime";
function isRetryableLinePushError(error: unknown): boolean {
const candidates = collectErrorGraphCandidates(error, (candidate) => [
candidate.cause,
candidate.error,
]);
const httpError = candidates.find(
(candidate): candidate is HTTPFetchError => candidate instanceof HTTPFetchError,
);
if (httpError) {
// LINE documents server errors and transport failures as the retriable
// outcomes; every 4xx (429 included) answers "retries don't change the result".
return httpError.status >= 500;
}
// A transport failure never reached a LINE response, so the retry key decides
// whether the earlier attempt already landed.
return candidates.some(
(candidate) => classifyTransientNetworkErrorCode(extractErrorCode(candidate)) !== undefined,
);
}
/**
* Pushes are non-idempotent without a retry key, so the generic message-matching
* fallback stays off and only the classification above may replay a request.
*/
export const runLinePushWithRetries = createChannelApiRetryRunner({
shouldRetry: isRetryableLinePushError,
strictShouldRetry: true,
verbose: true,
});