@openclaw/line 2026.7.2-beta.3 → 2026.7.2-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +1 -1
  2. package/dist/{accounts-BUBOoW5Q.js → accounts-CSH5sk8U.js} +9 -11
  3. package/dist/api.js +4 -3
  4. package/dist/{card-command-DQ-epsgt.js → card-command-BEMIm8D8.js} +1 -1
  5. package/dist/{channel-uT5RTtpi.js → channel-CTaWrzfl.js} +15 -11
  6. package/dist/channel-plugin-api.js +1 -1
  7. package/dist/channel.runtime-D4Paci2y.js +4 -0
  8. package/dist/contract-api.js +1 -1
  9. package/dist/{flex-templates-K1lS9i8L.js → flex-templates-DVy_wog4.js} +318 -107
  10. package/dist/index.js +1 -1
  11. package/dist/{markdown-to-line-D1vapcdP.js → markdown-to-line-Dn00k1bq.js} +239 -229
  12. package/dist/{monitor-Bk-lpoOZ.js → monitor-DzFU5UPV.js} +317 -323
  13. package/dist/monitor.runtime-B8xjVqev.js +2 -0
  14. package/dist/outbound.runtime-eHl7NNHu.js +2 -0
  15. package/dist/{probe-BslD77tJ.js → probe-CM03wo6r.js} +7 -9
  16. package/dist/probe.runtime-D13ZtXsd.js +2 -0
  17. package/dist/quick-reply-fallback-D86FwAfr.js +56 -0
  18. package/dist/{reply-payload-transform-r9sa-TYT.js → reply-payload-transform-BRM2fRvK.js} +3 -164
  19. package/dist/runtime-api.js +9 -8
  20. package/dist/send-receipt-CY2ey8XH.js +107 -0
  21. package/dist/setup-api.js +1 -1
  22. package/dist/{setup-surface-BEt4mazl.js → setup-surface-BHQOSD8R.js} +107 -67
  23. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupCreateRoute.d.ts.map +1 -1
  24. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupFailedType.d.ts.map +1 -1
  25. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupJobFailedType.d.ts.map +1 -1
  26. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupJobStatus.d.ts.map +1 -1
  27. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupStatus.d.ts.map +1 -1
  28. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexBoxBorderWidth.d.ts.map +1 -1
  29. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexBoxCornerRadius.d.ts.map +1 -1
  30. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexBubble.d.ts.map +1 -1
  31. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexIconSize.d.ts.map +1 -1
  32. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexSpanSize.d.ts.map +1 -1
  33. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexTextFontSize.d.ts.map +1 -1
  34. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/numberOfMessagesResponse.d.ts.map +1 -1
  35. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/postbackAction.d.ts.map +1 -1
  36. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/subscriptionPeriodDemographic.d.ts.map +1 -1
  37. package/node_modules/@line/bot-sdk/dist/cjs/version.d.ts +1 -1
  38. package/node_modules/@line/bot-sdk/dist/cjs/version.js +1 -1
  39. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/event.d.ts +2 -1
  40. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/event.d.ts.map +1 -1
  41. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.d.ts +24 -0
  42. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.d.ts.map +1 -0
  43. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.js +14 -0
  44. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.js.map +1 -0
  45. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.d.ts +1 -0
  46. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.d.ts.map +1 -1
  47. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.js +1 -0
  48. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.js.map +1 -1
  49. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupCreateRoute.d.ts.map +1 -1
  50. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupFailedType.d.ts.map +1 -1
  51. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupJobFailedType.d.ts.map +1 -1
  52. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupJobStatus.d.ts.map +1 -1
  53. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupStatus.d.ts.map +1 -1
  54. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexBoxBorderWidth.d.ts.map +1 -1
  55. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexBoxCornerRadius.d.ts.map +1 -1
  56. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexBubble.d.ts.map +1 -1
  57. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexIconSize.d.ts.map +1 -1
  58. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexSpanSize.d.ts.map +1 -1
  59. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexTextFontSize.d.ts.map +1 -1
  60. package/node_modules/@line/bot-sdk/dist/messaging-api/model/numberOfMessagesResponse.d.ts.map +1 -1
  61. package/node_modules/@line/bot-sdk/dist/messaging-api/model/postbackAction.d.ts.map +1 -1
  62. package/node_modules/@line/bot-sdk/dist/messaging-api/model/subscriptionPeriodDemographic.d.ts.map +1 -1
  63. package/node_modules/@line/bot-sdk/dist/version.d.ts +1 -1
  64. package/node_modules/@line/bot-sdk/dist/version.js +1 -1
  65. package/node_modules/@line/bot-sdk/dist/webhook/model/event.d.ts +2 -1
  66. package/node_modules/@line/bot-sdk/dist/webhook/model/event.d.ts.map +1 -1
  67. package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.d.ts +24 -0
  68. package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.d.ts.map +1 -0
  69. package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.js +13 -0
  70. package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.js.map +1 -0
  71. package/node_modules/@line/bot-sdk/dist/webhook/model/models.d.ts +1 -0
  72. package/node_modules/@line/bot-sdk/dist/webhook/model/models.d.ts.map +1 -1
  73. package/node_modules/@line/bot-sdk/dist/webhook/model/models.js +1 -0
  74. package/node_modules/@line/bot-sdk/dist/webhook/model/models.js.map +1 -1
  75. package/node_modules/@line/bot-sdk/lib/insight/model/getNumberOfFollowersResponse.ts +1 -1
  76. package/node_modules/@line/bot-sdk/lib/liff/model/liffScope.ts +1 -1
  77. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupCreateRoute.ts +1 -4
  78. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupFailedType.ts +1 -2
  79. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupJobFailedType.ts +1 -2
  80. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupJobStatus.ts +1 -4
  81. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupStatus.ts +1 -6
  82. package/node_modules/@line/bot-sdk/lib/messaging-api/model/botInfoResponse.ts +1 -1
  83. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexBoxBorderWidth.ts +1 -6
  84. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexBoxCornerRadius.ts +1 -7
  85. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexBubble.ts +1 -7
  86. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexIconSize.ts +1 -10
  87. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexSpanSize.ts +1 -10
  88. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexTextFontSize.ts +1 -10
  89. package/node_modules/@line/bot-sdk/lib/messaging-api/model/getAggregationUnitNameListResponse.ts +1 -1
  90. package/node_modules/@line/bot-sdk/lib/messaging-api/model/getFollowersResponse.ts +1 -1
  91. package/node_modules/@line/bot-sdk/lib/messaging-api/model/getJoinedMembershipUsersResponse.ts +1 -1
  92. package/node_modules/@line/bot-sdk/lib/messaging-api/model/issueLinkTokenResponse.ts +1 -1
  93. package/node_modules/@line/bot-sdk/lib/messaging-api/model/numberOfMessagesResponse.ts +2 -5
  94. package/node_modules/@line/bot-sdk/lib/messaging-api/model/postbackAction.ts +1 -4
  95. package/node_modules/@line/bot-sdk/lib/messaging-api/model/subscriptionPeriodDemographic.ts +1 -5
  96. package/node_modules/@line/bot-sdk/lib/version.ts +1 -1
  97. package/node_modules/@line/bot-sdk/lib/webhook/.openapi-generator/FILES +1 -0
  98. package/node_modules/@line/bot-sdk/lib/webhook/model/event.ts +2 -0
  99. package/node_modules/@line/bot-sdk/lib/webhook/model/messageEditedEvent.ts +32 -0
  100. package/node_modules/@line/bot-sdk/lib/webhook/model/models.ts +1 -0
  101. package/node_modules/@line/bot-sdk/lib/webhook/model/stickerMessageContent.ts +1 -1
  102. package/node_modules/@line/bot-sdk/package.json +3 -3
  103. package/node_modules/@types/node/README.md +1 -1
  104. package/node_modules/@types/node/fs/promises.d.ts +16 -5
  105. package/node_modules/@types/node/package.json +3 -3
  106. package/openclaw.plugin.json +0 -18
  107. package/package.json +71 -7
  108. package/dist/channel.runtime-BqNglqnr.js +0 -4
  109. package/dist/monitor.runtime-CHlqxGVL.js +0 -2
  110. package/dist/outbound.runtime-ByZ0Dsvu.js +0 -2
  111. package/dist/probe.runtime-CRhVGqwl.js +0 -2
  112. package/npm-shrinkwrap.json +0 -60
@@ -1,22 +1,23 @@
1
- import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-BUBOoW5Q.js";
2
- import { a as buildLineMediaMessage, i as buildLineQuickReplyFallbackText, l as getLineRuntime, m as resolveLineGroupConfigEntry, o as hasLineSpecificMediaOptions } from "./reply-payload-transform-r9sa-TYT.js";
3
- import { A as buildTemplateMessageFromPayload, C as pushMessagesLine, E as replyMessageLine, O as showLoadingAnimation, S as pushMessageLine, T as pushTextMessageWithQuickReplies, _ as getUserDisplayName, c as processLineMessage, d as createFlexMessage, h as createTextMessageWithQuickReplies, m as createQuickReplyItems, p as createLocationMessage } from "./markdown-to-line-D1vapcdP.js";
1
+ import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-CSH5sk8U.js";
2
+ import { a as resolveLineGroupConfigEntry, n as getLineRuntime, t as buildLineQuickReplyFallbackText } from "./quick-reply-fallback-D86FwAfr.js";
3
+ import { n as buildLineMediaMessage, r as hasLineSpecificMediaOptions } from "./send-receipt-CY2ey8XH.js";
4
+ import { E as buildTemplateMessageFromPayload, S as replyMessageLine, i as processLineMessage, l as createLocationMessage, p as getUserDisplayName, s as createFlexMessage, u as createQuickReplyItems, v as pushMessageLine, w as showLoadingAnimation, y as pushMessagesLine } from "./markdown-to-line-Dn00k1bq.js";
4
5
  import { createChannelPairingChallengeIssuer } from "openclaw/plugin-sdk/channel-pairing";
5
6
  import { normalizeOptionalString, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
6
- import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressDrain } from "openclaw/plugin-sdk/channel-outbound";
7
+ import { DEFAULT_INGRESS_ADOPTION_STALL_MS, DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS, bindIngressLifecycleToReplyOptions, createChannelIngressMonitor } from "openclaw/plugin-sdk/channel-outbound";
7
8
  import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
8
9
  import { sanitizeAssistantVisibleText } from "openclaw/plugin-sdk/text-chunking";
9
10
  import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
10
11
  import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
11
12
  import { firstDefined } from "openclaw/plugin-sdk/allow-from";
12
- import { Readable } from "node:stream";
13
- import { finished } from "node:stream/promises";
14
13
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
14
+ import { MediaFetchError } from "openclaw/plugin-sdk/media-runtime";
15
15
  import { saveMediaStream } from "openclaw/plugin-sdk/media-store";
16
- import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForAbortSignal } from "openclaw/plugin-sdk/runtime-env";
16
+ import { createNonExitingRuntime, danger, logVerbose, shouldLogVerbose, waitForAbortSignal, warn } from "openclaw/plugin-sdk/runtime-env";
17
17
  import { fetchWithRuntimeDispatcherOrMockedGlobal } from "openclaw/plugin-sdk/runtime-fetch";
18
+ import { HTTPFetchError } from "@line/bot-sdk";
18
19
  import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
19
- import { buildMentionRegexes, formatInboundEnvelope, formatInboundMediaUnavailableText, formatLocationText, hasFinalInboundReplyDispatch, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
20
+ import { buildChannelInboundEventContext, buildMentionRegexes, formatInboundEnvelope, formatInboundMediaUnavailableText, formatLocationText, hasFinalInboundReplyDispatch, matchesMentionPatterns, resolveInboundSessionEnvelopeContext, toInboundMediaFacts, toLocationContext } from "openclaw/plugin-sdk/channel-inbound";
20
21
  import { chunkMarkdownText } from "openclaw/plugin-sdk/reply-runtime";
21
22
  import { isRequestBodyLimitError, normalizePluginHttpPath, normalizeWebhookPath, registerWebhookTargetWithPluginRoute, requestBodyErrorToText, resolveSingleWebhookTarget } from "openclaw/plugin-sdk/webhook-ingress";
22
23
  import { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isRequestBodyLimitError as isRequestBodyLimitError$1, readRequestBodyWithLimit, requestBodyErrorToText as requestBodyErrorToText$1, runDetachedWebhookWork } from "openclaw/plugin-sdk/webhook-request-guards";
@@ -27,7 +28,6 @@ import { hasControlCommand } from "openclaw/plugin-sdk/command-auth-native";
27
28
  import { ensureConfiguredBindingRouteReady, readChannelAllowFromStore, resolveConfiguredBindingRoute, resolvePairingIdLabel, resolvePinnedMainDmOwnerFromAllowlist, resolveRuntimeConversationBindingRoute, upsertChannelPairingRequest } from "openclaw/plugin-sdk/conversation-runtime";
28
29
  import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
29
30
  import { resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
30
- import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
31
31
  import crypto from "node:crypto";
32
32
  import { safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
33
33
  //#region extensions/line/src/bot-access.ts
@@ -52,44 +52,85 @@ const CONTENT_READY_BASE_DELAY_MS = 500;
52
52
  const CONTENT_READY_MAX_DELAY_MS = 4e3;
53
53
  const CONTENT_READY_TIMEOUT_MS = 15e3;
54
54
  const LINE_CONTENT_BASE_URL = "https://api-data.line.me/v2/bot/message";
55
+ var RetryableLineMediaFetchError = class extends MediaFetchError {
56
+ constructor(message, options) {
57
+ super("fetch_failed", message, options);
58
+ this.name = "RetryableLineMediaFetchError";
59
+ }
60
+ };
55
61
  function contentBackoffDelayMs(attempt) {
56
62
  return Math.min(CONTENT_READY_BASE_DELAY_MS * 2 ** attempt, CONTENT_READY_MAX_DELAY_MS);
57
63
  }
64
+ function lineContentUrl(messageId) {
65
+ return `${LINE_CONTENT_BASE_URL}/${encodeURIComponent(messageId)}/content`;
66
+ }
67
+ async function* lineResponseBodyChunks(response, messageId) {
68
+ const body = response.body;
69
+ if (!body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
70
+ const reader = body.getReader();
71
+ let completed = false;
72
+ try {
73
+ while (true) {
74
+ let chunk;
75
+ try {
76
+ chunk = await reader.read();
77
+ } catch (err) {
78
+ throw new RetryableLineMediaFetchError(`LINE media response stream failed for message ${messageId}`, { cause: err });
79
+ }
80
+ if (chunk.done) {
81
+ completed = true;
82
+ return;
83
+ }
84
+ if (chunk.value.byteLength > 0) yield chunk.value;
85
+ }
86
+ } finally {
87
+ if (!completed) await reader.cancel().catch(() => void 0);
88
+ try {
89
+ reader.releaseLock();
90
+ } catch {}
91
+ }
92
+ }
58
93
  async function fetchLineContentWhenReady(messageId, channelAccessToken) {
59
94
  const controller = new AbortController();
60
95
  const deadline = setTimeout(() => controller.abort(), CONTENT_READY_TIMEOUT_MS);
61
96
  deadline.unref();
62
97
  try {
63
98
  for (let attempt = 0; attempt < CONTENT_READY_MAX_ATTEMPTS; attempt++) {
64
- const response = await fetchWithRuntimeDispatcherOrMockedGlobal(`${LINE_CONTENT_BASE_URL}/${encodeURIComponent(messageId)}/content`, {
99
+ const response = await fetchWithRuntimeDispatcherOrMockedGlobal(lineContentUrl(messageId), {
65
100
  headers: { Authorization: `Bearer ${channelAccessToken}` },
66
101
  redirect: "error",
67
102
  signal: controller.signal
68
103
  });
69
104
  if (response.status === 200) {
70
- if (!response.body) throw new Error(`LINE media response for message ${messageId} had no body`);
71
- return Readable.fromWeb(response.body);
105
+ if (!response.body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
106
+ return response;
72
107
  }
73
- await response.body?.cancel();
74
- if (response.status !== 202) throw new Error(`LINE media download failed for message ${messageId} (HTTP ${response.status})`);
108
+ await response.body?.cancel().catch(() => void 0);
109
+ if (response.status !== 202) throw new MediaFetchError("http_error", `LINE media download failed for message ${messageId} (HTTP ${response.status})`, { status: response.status });
75
110
  if (attempt < CONTENT_READY_MAX_ATTEMPTS - 1) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal: controller.signal });
76
111
  }
77
112
  } catch (err) {
78
- if (controller.signal.aborted) throw new Error(`LINE media for message ${messageId} did not become ready within ${CONTENT_READY_TIMEOUT_MS / 1e3} seconds`, { cause: err });
79
- throw err;
113
+ if (controller.signal.aborted) throw new RetryableLineMediaFetchError(`LINE media for message ${messageId} did not become ready within ${CONTENT_READY_TIMEOUT_MS / 1e3} seconds`, { cause: err });
114
+ if (err instanceof MediaFetchError) throw err;
115
+ throw new RetryableLineMediaFetchError(`LINE media download failed for message ${messageId}`, { cause: err });
80
116
  } finally {
81
117
  clearTimeout(deadline);
82
118
  }
83
- throw new Error(`LINE media for message ${messageId} was still preparing (HTTP 202) after ${CONTENT_READY_MAX_ATTEMPTS} attempts`);
119
+ throw new MediaFetchError("http_error", `LINE media for message ${messageId} was still preparing (HTTP 202) after ${CONTENT_READY_MAX_ATTEMPTS} attempts`, { status: 202 });
120
+ }
121
+ function isRetryableLineInboundMediaError(err) {
122
+ if (err instanceof RetryableLineMediaFetchError) return true;
123
+ if (!(err instanceof MediaFetchError)) return false;
124
+ if (err.code === "http_error") return err.status === 202 || err.status === 408 || err.status === 429 || typeof err.status === "number" && err.status >= 500;
125
+ return false;
84
126
  }
85
127
  async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 * 1024 * 1024, options) {
86
- const content = await fetchLineContentWhenReady(messageId, channelAccessToken);
128
+ const response = await fetchLineContentWhenReady(messageId, channelAccessToken);
87
129
  let saved;
88
130
  try {
89
- saved = await saveMediaStream(content, void 0, "inbound", maxBytes, options?.originalFilename);
131
+ saved = await saveMediaStream(lineResponseBodyChunks(response, messageId), response.headers.get("content-type") ?? void 0, "inbound", maxBytes, options?.originalFilename);
90
132
  } catch (err) {
91
- content.destroy();
92
- await finished(content).catch(() => void 0);
133
+ await response.body?.cancel().catch(() => void 0);
93
134
  throw err;
94
135
  }
95
136
  logVerbose(`line: persisted media ${messageId} to ${saved.path} (${saved.size} bytes)`);
@@ -102,7 +143,11 @@ async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 *
102
143
  //#endregion
103
144
  //#region extensions/line/src/auto-reply-delivery.ts
104
145
  function toLineDeliveryError(error) {
105
- return error instanceof Error ? error : new Error("LINE rich or media message send failed", { cause: error });
146
+ return error instanceof Error ? error : new Error("LINE message send failed", { cause: error });
147
+ }
148
+ function getLineHttpError(error) {
149
+ if (error instanceof HTTPFetchError) return error;
150
+ return error instanceof Error && error.cause instanceof HTTPFetchError ? error.cause : void 0;
106
151
  }
107
152
  function markLineVisibleDeliveryError(error) {
108
153
  const deliveryError = toLineDeliveryError(error);
@@ -113,7 +158,7 @@ function markLineVisibleDeliveryError(error) {
113
158
  });
114
159
  return deliveryError;
115
160
  }
116
- const visibleError = new Error("LINE rich or media message send failed", { cause: deliveryError });
161
+ const visibleError = new Error("LINE message send failed", { cause: deliveryError });
117
162
  Object.assign(visibleError, {
118
163
  sentBeforeError: true,
119
164
  visibleReplySent: true
@@ -135,14 +180,25 @@ async function deliverLineAutoReply(params) {
135
180
  }
136
181
  };
137
182
  const replyVisible = (...args) => sendVisible(() => deps.replyMessageLine(...args));
138
- const pushTextVisible = (...args) => sendVisible(() => deps.pushMessageLine(...args));
139
- const pushQuickRepliesVisible = (...args) => sendVisible(() => deps.pushTextMessageWithQuickReplies(...args));
140
- const pushLineMessages = async (messages) => {
183
+ const failedPushSegments = /* @__PURE__ */ new WeakMap();
184
+ const pushLineMessages = async (messages, allowFailedBatchTextRecovery, externalTail = []) => {
141
185
  if (messages.length === 0) return;
142
- for (let i = 0; i < messages.length; i += 5) await sendVisible(() => deps.pushMessagesLine(to, messages.slice(i, i + 5), {
143
- cfg: params.cfg,
144
- accountId
145
- }));
186
+ for (let i = 0; i < messages.length; i += 5) {
187
+ const batch = messages.slice(i, i + 5);
188
+ try {
189
+ await sendVisible(() => deps.pushMessagesLine(to, batch, {
190
+ cfg: params.cfg,
191
+ accountId
192
+ }));
193
+ } catch (error) {
194
+ if (typeof error === "object" && error !== null) failedPushSegments.set(error, {
195
+ allowFailedBatchTextRecovery,
196
+ failedBatch: batch,
197
+ unattemptedTail: [...messages.slice(i + batch.length), ...externalTail]
198
+ });
199
+ throw error;
200
+ }
201
+ }
146
202
  };
147
203
  const sendLineMessages = async (messages, allowReplyToken) => {
148
204
  if (messages.length === 0) return;
@@ -156,12 +212,13 @@ async function deliverLineAutoReply(params) {
156
212
  });
157
213
  } catch (err) {
158
214
  deps.onReplyError?.(err);
159
- await pushLineMessages(replyBatch);
215
+ await pushLineMessages(replyBatch, getLineHttpError(err)?.status === 400, remaining.slice(replyBatch.length));
216
+ } finally {
217
+ replyTokenUsed = true;
160
218
  }
161
- replyTokenUsed = true;
162
219
  remaining = remaining.slice(replyBatch.length);
163
220
  }
164
- if (remaining.length > 0) await pushLineMessages(remaining);
221
+ if (remaining.length > 0) await pushLineMessages(remaining, true);
165
222
  };
166
223
  const richMessages = [];
167
224
  const hasQuickReplies = Boolean(lineData.quickReplies?.length);
@@ -186,82 +243,59 @@ async function deliverLineAutoReply(params) {
186
243
  trackingId: lineData.trackingId
187
244
  };
188
245
  const mediaMessages = [];
189
- let richMediaError;
246
+ let deliveryError;
190
247
  for (const rawUrl of mediaUrls) {
191
248
  const url = rawUrl?.trim();
192
249
  if (!url) continue;
193
250
  try {
194
251
  mediaMessages.push(await deps.buildMediaMessage(url, mediaOpts, to));
195
252
  } catch (err) {
196
- richMediaError ??= err;
253
+ deliveryError ??= err;
197
254
  }
198
255
  }
199
- if (chunks.length > 0) {
200
- const hasRichOrMedia = richMessages.length > 0 || mediaMessages.length > 0;
201
- const sendRichBeforeText = hasQuickReplies && hasRichOrMedia;
202
- if (sendRichBeforeText) try {
203
- await sendLineMessages([...richMessages, ...mediaMessages], false);
204
- } catch (err) {
205
- richMediaError ??= err;
206
- }
207
- const { replyTokenUsed: nextReplyTokenUsed } = await deps.sendLineReplyChunks({
208
- to,
209
- chunks,
210
- quickReplies: lineData.quickReplies,
211
- replyToken,
212
- replyTokenUsed,
213
- cfg: params.cfg,
214
- accountId,
215
- replyMessageLine: replyVisible,
216
- pushMessageLine: pushTextVisible,
217
- pushTextMessageWithQuickReplies: pushQuickRepliesVisible,
218
- createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies,
219
- onReplyError: deps.onReplyError
220
- });
221
- replyTokenUsed = nextReplyTokenUsed;
222
- if (!sendRichBeforeText) try {
223
- await sendLineMessages(richMessages, false);
224
- if (mediaMessages.length > 0) await sendLineMessages(mediaMessages, false);
225
- } catch (err) {
226
- richMediaError ??= err;
227
- }
228
- } else {
229
- const combined = [...richMessages, ...mediaMessages];
230
- if (hasQuickReplies && combined.length === 0) {
231
- const { replyTokenUsed: nextReplyTokenUsed } = await deps.sendLineReplyChunks({
232
- to,
233
- chunks: [buildLineQuickReplyFallbackText(lineData.quickReplies)],
234
- quickReplies: lineData.quickReplies,
235
- replyToken,
236
- replyTokenUsed,
237
- cfg: params.cfg,
238
- accountId,
239
- replyMessageLine: replyVisible,
240
- pushMessageLine: pushTextVisible,
241
- pushTextMessageWithQuickReplies: pushQuickRepliesVisible,
242
- createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies,
243
- onReplyError: deps.onReplyError
244
- });
245
- replyTokenUsed = nextReplyTokenUsed;
246
- } else {
247
- if (hasQuickReplies && combined.length > 0) {
248
- const quickReply = deps.createQuickReplyItems(lineData.quickReplies);
249
- const targetIndex = replyToken && !replyTokenUsed ? Math.min(4, combined.length - 1) : combined.length - 1;
250
- combined[targetIndex] = {
251
- ...combined[targetIndex],
252
- quickReply
253
- };
254
- }
255
- await sendLineMessages(combined, true);
256
- }
256
+ const textMessages = chunks.map((text) => ({
257
+ type: "text",
258
+ text
259
+ }));
260
+ const richMediaMessages = [...richMessages, ...mediaMessages];
261
+ if (hasQuickReplies && textMessages.length === 0 && richMediaMessages.length === 0) textMessages.push({
262
+ type: "text",
263
+ text: buildLineQuickReplyFallbackText(lineData.quickReplies)
264
+ });
265
+ if (hasQuickReplies) {
266
+ const targetMessages = textMessages.length > 0 ? textMessages : richMediaMessages;
267
+ const lastIndex = targetMessages.length - 1;
268
+ targetMessages[lastIndex] = {
269
+ ...expectDefined(targetMessages[lastIndex], "last LINE auto-reply message"),
270
+ quickReply: deps.createQuickReplyItems(lineData.quickReplies)
271
+ };
257
272
  }
258
- if (richMediaError !== void 0) {
259
- if (!visibleReplySent) throw toLineDeliveryError(richMediaError);
273
+ const messages = hasQuickReplies ? [...richMediaMessages, ...textMessages] : [...textMessages, ...richMediaMessages];
274
+ try {
275
+ await sendLineMessages(messages, true);
276
+ } catch (err) {
277
+ deliveryError ??= err;
278
+ const failedSegment = typeof err === "object" && err !== null ? failedPushSegments.get(err) : void 0;
279
+ const httpError = getLineHttpError(err);
280
+ const retryCandidates = failedSegment ? [...failedSegment.allowFailedBatchTextRecovery ? failedSegment.failedBatch : [], ...failedSegment.unattemptedTail] : [];
281
+ const retryTextMessages = retryCandidates.filter((message) => message.type === "text");
282
+ const quickRepliesNeedCarrier = hasQuickReplies && retryCandidates.some((message) => "quickReply" in message);
283
+ const retryMessages = retryTextMessages.length > 0 ? retryTextMessages : quickRepliesNeedCarrier ? [{
284
+ type: "text",
285
+ text: buildLineQuickReplyFallbackText(lineData.quickReplies),
286
+ quickReply: deps.createQuickReplyItems(lineData.quickReplies)
287
+ }] : [];
288
+ if (retryMessages.length > 0 && failedSegment?.failedBatch.some((message) => message.type !== "text") && httpError?.status === 400) try {
289
+ await sendLineMessages(retryMessages, false);
290
+ } catch {}
291
+ }
292
+ if (deliveryError !== void 0) {
293
+ if (!visibleReplySent) throw toLineDeliveryError(deliveryError);
260
294
  return {
261
295
  status: "partial",
262
296
  replyTokenUsed,
263
297
  visibleReplySent: true,
264
- error: markLineVisibleDeliveryError(richMediaError)
298
+ error: markLineVisibleDeliveryError(deliveryError)
265
299
  };
266
300
  }
267
301
  return {
@@ -397,48 +431,27 @@ function extractMessageText(message) {
397
431
  }
398
432
  return "";
399
433
  }
400
- function extractMediaPlaceholder(message) {
434
+ function extractNativeMediaKind(message) {
401
435
  switch (message.type) {
402
- case "image": return "<media:image>";
403
- case "video": return "<media:video>";
404
- case "audio": return "<media:audio>";
405
- case "file": return "<media:document>";
406
- default: return "";
436
+ case "image": return "image";
437
+ case "video": return "video";
438
+ case "audio": return "audio";
439
+ case "file": return "document";
440
+ default: return;
407
441
  }
408
442
  }
409
- function resolveLineConversationLabel(params) {
410
- return params.isGroup ? params.groupId ? `group:${params.groupId}` : params.roomId ? `room:${params.roomId}` : "unknown-group" : params.senderLabel;
411
- }
412
- function resolveLineAddresses(params) {
413
- const fromAddress = params.isGroup ? params.groupId ? `line:group:${params.groupId}` : params.roomId ? `line:room:${params.roomId}` : `line:${params.peerId}` : `line:${params.userId ?? params.peerId}`;
414
- return {
415
- fromAddress,
416
- toAddress: params.isGroup ? fromAddress : `line:${params.userId ?? params.peerId}`,
417
- originatingTo: params.isGroup ? fromAddress : `line:${params.userId ?? params.peerId}`
418
- };
419
- }
420
443
  async function finalizeLineInboundContext(params) {
421
- const { fromAddress, toAddress, originatingTo } = resolveLineAddresses({
422
- isGroup: params.source.isGroup,
423
- groupId: params.source.groupId,
424
- roomId: params.source.roomId,
425
- userId: params.source.userId,
426
- peerId: params.source.peerId
427
- });
428
444
  const senderId = params.source.userId ?? "unknown";
429
445
  const senderLabel = params.source.userId ? `user:${params.source.userId}` : "unknown";
430
- const conversationLabel = resolveLineConversationLabel({
431
- isGroup: params.source.isGroup,
432
- groupId: params.source.groupId,
433
- roomId: params.source.roomId,
434
- senderLabel
435
- });
446
+ const conversationLabel = params.source.isGroup ? params.source.groupId ? `group:${params.source.groupId}` : params.source.roomId ? `room:${params.source.roomId}` : "unknown-group" : senderLabel;
447
+ const address = params.source.groupId ? `line:group:${params.source.groupId}` : params.source.roomId ? `line:room:${params.source.roomId}` : `line:${params.source.userId ?? params.source.peerId}`;
436
448
  const { storePath, envelopeOptions, previousTimestamp } = resolveInboundSessionEnvelopeContext({
437
449
  cfg: params.cfg,
438
450
  agentId: params.route.agentId,
439
451
  sessionKey: params.route.sessionKey
440
452
  });
441
453
  const agentBody = params.agentBody ?? params.rawBody;
454
+ const media = toInboundMediaFacts(params.media);
442
455
  const body = formatInboundEnvelope({
443
456
  channel: "LINE",
444
457
  from: conversationLabel,
@@ -449,39 +462,45 @@ async function finalizeLineInboundContext(params) {
449
462
  previousTimestamp,
450
463
  envelope: envelopeOptions
451
464
  });
452
- const ctxPayload = finalizeInboundContext({
453
- Body: body,
454
- BodyForAgent: agentBody,
455
- RawBody: params.rawBody,
456
- CommandBody: params.rawBody,
457
- From: fromAddress,
458
- To: toAddress,
459
- SessionKey: params.route.sessionKey,
460
- DmScope: params.route.dmScope,
461
- AccountId: params.route.accountId,
462
- ChatType: params.source.isGroup ? "group" : "direct",
463
- ConversationLabel: conversationLabel,
464
- GroupSubject: params.source.isGroup ? params.source.groupId ?? params.source.roomId : void 0,
465
- SenderId: senderId,
466
- Provider: "line",
467
- Surface: "line",
468
- MessageSid: params.messageSid,
469
- Timestamp: params.timestamp,
470
- MediaPath: params.media.firstPath,
471
- MediaType: params.media.firstContentType,
472
- MediaUrl: params.media.firstPath,
473
- MediaPaths: params.media.paths,
474
- MediaUrls: params.media.paths,
475
- MediaTypes: params.media.types,
476
- ...params.locationContext,
477
- CommandAuthorized: params.commandAuthorized,
478
- OriginatingChannel: "line",
479
- OriginatingTo: originatingTo,
480
- GroupSystemPrompt: params.source.isGroup ? normalizeOptionalString(resolveLineGroupConfigEntry(params.account.config.groups, {
481
- groupId: params.source.groupId,
482
- roomId: params.source.roomId
483
- })?.systemPrompt) : void 0,
484
- InboundHistory: params.inboundHistory
465
+ const ctxPayload = buildChannelInboundEventContext({
466
+ channel: "line",
467
+ accountId: params.route.accountId,
468
+ messageId: params.messageSid,
469
+ timestamp: params.timestamp,
470
+ from: address,
471
+ sender: { id: senderId },
472
+ conversation: {
473
+ kind: params.source.isGroup ? "group" : "direct",
474
+ id: params.source.peerId,
475
+ label: conversationLabel
476
+ },
477
+ route: {
478
+ agentId: params.route.agentId,
479
+ dmScope: params.route.dmScope,
480
+ accountId: params.route.accountId,
481
+ routeSessionKey: params.route.sessionKey
482
+ },
483
+ reply: {
484
+ to: address,
485
+ originatingTo: address
486
+ },
487
+ message: {
488
+ body,
489
+ bodyForAgent: agentBody,
490
+ rawBody: params.rawBody,
491
+ commandBody: params.rawBody,
492
+ inboundHistory: params.inboundHistory
493
+ },
494
+ access: { commands: { authorized: params.commandAuthorized } },
495
+ media,
496
+ extra: {
497
+ ...params.locationContext,
498
+ GroupSubject: params.source.isGroup ? params.source.groupId ?? params.source.roomId : void 0,
499
+ GroupSystemPrompt: params.source.isGroup ? normalizeOptionalString(resolveLineGroupConfigEntry(params.account.config.groups, {
500
+ groupId: params.source.groupId,
501
+ roomId: params.source.roomId
502
+ })?.systemPrompt) : void 0
503
+ }
485
504
  });
486
505
  const pinnedMainDmOwner = !params.source.isGroup ? resolvePinnedMainDmOwnerFromAllowlist({
487
506
  dmScope: params.cfg.session?.dmScope,
@@ -535,15 +554,17 @@ async function buildLineMessageContext(params) {
535
554
  const messageId = message.id;
536
555
  const timestamp = event.timestamp;
537
556
  const textContent = extractMessageText(message);
538
- const placeholder = extractMediaPlaceholder(message);
539
- let rawBody = textContent || placeholder;
540
- if (!rawBody && allMedia.length > 0) rawBody = `<media:image>${allMedia.length > 1 ? ` (${allMedia.length} images)` : ""}`;
557
+ const nativeMediaKind = extractNativeMediaKind(message);
558
+ const mediaFacts = allMedia.length > 0 ? allMedia.map((media) => ({
559
+ ...media,
560
+ kind: nativeMediaKind
561
+ })) : nativeMediaKind ? [{ kind: nativeMediaKind }] : [];
562
+ const rawBody = textContent;
541
563
  const agentBody = mediaUnavailable ? formatInboundMediaUnavailableText({
542
564
  body: rawBody,
543
- mediaPlaceholder: placeholder,
544
565
  notice: "[line attachment unavailable]"
545
566
  }) : rawBody;
546
- if (!agentBody && allMedia.length === 0) return null;
567
+ if (!agentBody && mediaFacts.length === 0) return null;
547
568
  let locationContext;
548
569
  if (message.type === "location") {
549
570
  const loc = message;
@@ -571,12 +592,7 @@ async function buildLineMessageContext(params) {
571
592
  timestamp,
572
593
  messageSid: messageId,
573
594
  commandAuthorized,
574
- media: {
575
- firstPath: allMedia[0]?.path,
576
- firstContentType: allMedia[0]?.contentType,
577
- paths: allMedia.length > 0 ? allMedia.map((m) => m.path) : void 0,
578
- types: allMedia.length > 0 ? allMedia.map((m) => m.contentType).filter(Boolean) : void 0
579
- },
595
+ media: mediaFacts,
580
596
  locationContext,
581
597
  verboseLog: {
582
598
  kind: "inbound",
@@ -632,12 +648,7 @@ async function buildLinePostbackContext(params) {
632
648
  timestamp,
633
649
  messageSid,
634
650
  commandAuthorized,
635
- media: {
636
- firstPath: "",
637
- firstContentType: void 0,
638
- paths: void 0,
639
- types: void 0
640
- },
651
+ media: [],
641
652
  verboseLog: { kind: "postback" }
642
653
  });
643
654
  return {
@@ -952,6 +963,7 @@ async function handleMessageEvent(event, context) {
952
963
  contentType: media.contentType
953
964
  });
954
965
  } catch (err) {
966
+ if (isRetryableLineInboundMediaError(err)) throw err;
955
967
  mediaUnavailable = true;
956
968
  const errMsg = String(err);
957
969
  if (errMsg.includes("exceeds") && errMsg.includes("limit")) logVerbose(`line: media exceeds size limit for message ${message.id}`);
@@ -1052,6 +1064,7 @@ const LINE_WEBHOOK_SPOOL_VERSION = 1;
1052
1064
  const LINE_WEBHOOK_DRAIN_INTERVAL_MS = 500;
1053
1065
  const LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
1054
1066
  const LINE_WEBHOOK_DRAIN_SCAN_LIMIT = 100;
1067
+ const LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS = 5e3;
1055
1068
  const LINE_WEBHOOK_TOMBSTONE_TTL_MS = 720 * 60 * 6e4;
1056
1069
  const LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES = 4096;
1057
1070
  var LineWebhookPayloadError = class extends Error {
@@ -1099,15 +1112,13 @@ function laneKeyFor(event, eventId) {
1099
1112
  }
1100
1113
  return eventId;
1101
1114
  }
1102
- function parseClaimedEvent(payload, claimedId) {
1103
- if (payload.version !== LINE_WEBHOOK_SPOOL_VERSION || typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError("LINE webhook spool payload is invalid.");
1115
+ function parseStoredEvent(rawEvent) {
1104
1116
  let event;
1105
1117
  try {
1106
- event = JSON.parse(payload.rawEvent);
1118
+ event = JSON.parse(rawEvent);
1107
1119
  } catch (error) {
1108
1120
  throw new LineWebhookPayloadError("LINE webhook event JSON is invalid.", { cause: error });
1109
1121
  }
1110
- if (eventIdFor(event) !== claimedId) throw new LineWebhookPayloadError("LINE webhook event id changed after durable admission.");
1111
1122
  return event;
1112
1123
  }
1113
1124
  function errorText(error) {
@@ -1118,41 +1129,61 @@ function isLineAuthenticationFailure(error) {
1118
1129
  const status = error.status;
1119
1130
  return status === 401 || status === 403;
1120
1131
  }
1132
+ async function waitForActiveDeliveriesBeforeDispose(activeDeliveries) {
1133
+ let timeout;
1134
+ try {
1135
+ return await Promise.race([Promise.allSettled(activeDeliveries).then(() => true), new Promise((resolve) => {
1136
+ timeout = setTimeout(() => resolve(false), LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS);
1137
+ timeout.unref?.();
1138
+ })]);
1139
+ } finally {
1140
+ if (timeout) clearTimeout(timeout);
1141
+ }
1142
+ }
1121
1143
  function createLineWebhookSpool(options) {
1122
1144
  const queue = options.queue ?? getLineRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
1123
- const shutdown = new AbortController();
1124
1145
  const activeDeliveries = /* @__PURE__ */ new Set();
1125
1146
  const deferredClaims = /* @__PURE__ */ new Map();
1126
- const drain = createChannelIngressDrain({
1147
+ let acceptsDeferredClaims = true;
1148
+ const monitor = createChannelIngressMonitor({
1127
1149
  queue,
1128
- abortSignal: shutdown.signal,
1129
- adoptionStallTimeoutMs: DEFAULT_INGRESS_ADOPTION_STALL_MS,
1130
- orderBy: "received",
1131
- scanLimit: LINE_WEBHOOK_DRAIN_SCAN_LIMIT,
1132
- startLimit: LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES,
1133
- retryPolicy: {
1134
- maxAttempts: DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
1135
- deadLetterMinAgeMs: 0
1136
- },
1137
- resolveNonRetryableFailure: (error) => {
1138
- if (error instanceof LineWebhookPayloadError) return {
1139
- reason: "invalid-event",
1140
- message: error.message
1150
+ inspect: ({ event }) => {
1151
+ const eventId = eventIdFor(event);
1152
+ return {
1153
+ eventId,
1154
+ laneKey: laneKeyFor(event, eventId)
1141
1155
  };
1142
- if (error instanceof LineWebhookTerminalDeliveryError) return {
1143
- reason: error.reason,
1144
- message: error.message
1145
- };
1146
- if (isLineAuthenticationFailure(error)) return {
1147
- reason: "authentication-failed",
1148
- message: errorText(error)
1149
- };
1150
- return null;
1151
1156
  },
1152
- onLog: (message) => options.runtime.error?.(danger(`line: ${message}`)),
1153
- dispatchClaimedEvent: async (claimed, lifecycle) => {
1154
- const event = parseClaimedEvent(claimed.payload, claimed.id);
1157
+ payload: {
1158
+ version: LINE_WEBHOOK_SPOOL_VERSION,
1159
+ serialize: ({ event, destination }) => ({
1160
+ rawEvent: JSON.stringify(event),
1161
+ destination
1162
+ }),
1163
+ deserialize: ({ rawEvent, destination }) => ({
1164
+ event: parseStoredEvent(rawEvent),
1165
+ destination
1166
+ }),
1167
+ encode: ({ version, body }) => ({
1168
+ version,
1169
+ rawEvent: body.rawEvent,
1170
+ destination: body.destination
1171
+ }),
1172
+ decode: (payload) => {
1173
+ if (typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError("LINE webhook spool payload is invalid.");
1174
+ return {
1175
+ version: payload.version,
1176
+ body: {
1177
+ rawEvent: payload.rawEvent,
1178
+ destination: payload.destination
1179
+ }
1180
+ };
1181
+ },
1182
+ createClaimError: (kind) => new LineWebhookPayloadError(kind === "invalid-version" ? "LINE webhook spool payload is invalid." : "LINE webhook event identity changed after durable admission.")
1183
+ },
1184
+ deliver: async ({ event, destination }, lifecycle, claim) => {
1155
1185
  const boundLifecycle = bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle;
1186
+ let handedOff = false;
1156
1187
  let resolveDeferredClaim;
1157
1188
  const deferredClaim = new Promise((resolve) => {
1158
1189
  resolveDeferredClaim = resolve;
@@ -1161,12 +1192,13 @@ function createLineWebhookSpool(options) {
1161
1192
  const settleDeferredClaim = () => {
1162
1193
  if (deferredClaimSettled) return;
1163
1194
  deferredClaimSettled = true;
1164
- if (deferredClaims.get(claimed.id) === deferredClaim) deferredClaims.delete(claimed.id);
1195
+ if (deferredClaims.get(claim.id) === deferredClaim) deferredClaims.delete(claim.id);
1165
1196
  resolveDeferredClaim();
1166
1197
  };
1167
- const delivery = options.deliver(event, claimed.payload.destination, { turnAdoptionLifecycle: {
1198
+ const delivery = options.deliver(event, destination, { turnAdoptionLifecycle: {
1168
1199
  ...boundLifecycle,
1169
1200
  onAdopted: async () => {
1201
+ handedOff = true;
1170
1202
  try {
1171
1203
  await boundLifecycle.onAdopted();
1172
1204
  } finally {
@@ -1174,10 +1206,19 @@ function createLineWebhookSpool(options) {
1174
1206
  }
1175
1207
  },
1176
1208
  onDeferred: () => {
1177
- if (!deferredClaimSettled) deferredClaims.set(claimed.id, deferredClaim);
1209
+ handedOff = true;
1210
+ if (!acceptsDeferredClaims) {
1211
+ settleDeferredClaim();
1212
+ Promise.resolve().then(() => boundLifecycle.onAbandoned()).catch((error) => {
1213
+ options.runtime.error?.(danger(`line: failed to abandon a late webhook delivery: ${errorText(error)}`));
1214
+ });
1215
+ return;
1216
+ }
1217
+ if (!deferredClaimSettled) deferredClaims.set(claim.id, deferredClaim);
1178
1218
  boundLifecycle.onDeferred();
1179
1219
  },
1180
1220
  onAbandoned: async () => {
1221
+ handedOff = true;
1181
1222
  try {
1182
1223
  await boundLifecycle.onAbandoned();
1183
1224
  } finally {
@@ -1191,73 +1232,80 @@ function createLineWebhookSpool(options) {
1191
1232
  } finally {
1192
1233
  activeDeliveries.delete(delivery);
1193
1234
  }
1194
- }
1235
+ if (stopTask && !handedOff) return {
1236
+ kind: "failed-retryable",
1237
+ error: /* @__PURE__ */ new Error("LINE webhook spool stopped before delivery handoff.")
1238
+ };
1239
+ },
1240
+ pollIntervalMs: LINE_WEBHOOK_DRAIN_INTERVAL_MS,
1241
+ retention: {
1242
+ pruneIntervalMs: 0,
1243
+ completedTtlMs: LINE_WEBHOOK_TOMBSTONE_TTL_MS,
1244
+ completedMaxEntries: LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES,
1245
+ failedTtlMs: LINE_WEBHOOK_TOMBSTONE_TTL_MS,
1246
+ failedMaxEntries: LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES
1247
+ },
1248
+ appendRetryDelaysMs: [0],
1249
+ waitForDeliveryIdleBeforeRepump: false,
1250
+ waitForDeliveryIdleOnStop: false,
1251
+ runPumpTask: runDetachedWebhookWork,
1252
+ admissionMode: "durable-after-stop",
1253
+ drain: {
1254
+ adoptionStallTimeoutMs: DEFAULT_INGRESS_ADOPTION_STALL_MS,
1255
+ orderBy: "received",
1256
+ scanLimit: LINE_WEBHOOK_DRAIN_SCAN_LIMIT,
1257
+ startLimit: LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES,
1258
+ retryPolicy: {
1259
+ maxAttempts: DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
1260
+ deadLetterMinAgeMs: 0
1261
+ },
1262
+ resolveNonRetryableFailure: (error) => {
1263
+ if (error instanceof LineWebhookPayloadError) return {
1264
+ reason: "invalid-event",
1265
+ message: error.message
1266
+ };
1267
+ if (error instanceof LineWebhookTerminalDeliveryError) return {
1268
+ reason: error.reason,
1269
+ message: error.message
1270
+ };
1271
+ if (isLineAuthenticationFailure(error)) return {
1272
+ reason: "authentication-failed",
1273
+ message: errorText(error)
1274
+ };
1275
+ return null;
1276
+ },
1277
+ onLog: (message) => options.runtime.error?.(danger(`line: ${message}`))
1278
+ },
1279
+ createStoppedError: () => /* @__PURE__ */ new Error("LINE webhook spool is stopped."),
1280
+ onError: (error) => options.runtime.error?.(danger(`line: webhook spool drain failed: ${errorText(error)}`))
1195
1281
  });
1196
- let running = false;
1197
- let drainRequested = false;
1198
- let drainTask;
1199
- let drainTimer;
1200
- const requestDrain = () => {
1201
- if (!running || shutdown.signal.aborted) return;
1202
- drainRequested = true;
1203
- if (drainTask) return;
1204
- drainTask = runDetachedWebhookWork(async () => {
1205
- while (drainRequested && !shutdown.signal.aborted) {
1206
- if (!running) break;
1207
- drainRequested = false;
1208
- await drain.drainOnce({ shouldStop: () => !running || shutdown.signal.aborted || activeDeliveries.size >= LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES });
1209
- }
1210
- }).catch((error) => {
1211
- options.runtime.error?.(danger(`line: webhook spool drain failed: ${errorText(error)}`));
1212
- }).finally(() => {
1213
- drainTask = void 0;
1214
- if (running && drainRequested && !shutdown.signal.aborted) requestDrain();
1215
- });
1216
- };
1282
+ let stopTask;
1217
1283
  return {
1218
1284
  accept: async (body) => {
1219
1285
  const events = body.events ?? [];
1220
1286
  if (events.length === 0) return;
1221
- await queue.prune({
1222
- completedTtlMs: LINE_WEBHOOK_TOMBSTONE_TTL_MS,
1223
- completedMaxEntries: LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES,
1224
- failedTtlMs: LINE_WEBHOOK_TOMBSTONE_TTL_MS,
1225
- failedMaxEntries: LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES
1226
- });
1227
- const receivedAt = Date.now();
1228
- for (const event of events) {
1229
- const eventId = eventIdFor(event);
1230
- await queue.enqueue(eventId, {
1231
- version: LINE_WEBHOOK_SPOOL_VERSION,
1232
- rawEvent: JSON.stringify(event),
1233
- destination: body.destination ?? ""
1234
- }, {
1235
- receivedAt,
1236
- laneKey: laneKeyFor(event, eventId)
1237
- });
1238
- }
1239
- requestDrain();
1287
+ await monitor.admitBatch(events.map((event) => ({
1288
+ event,
1289
+ destination: body.destination ?? ""
1290
+ })), { receivedAt: Date.now() });
1240
1291
  },
1241
1292
  start: () => {
1242
- if (running) return;
1243
- running = true;
1244
- requestDrain();
1245
- drainTimer = setInterval(requestDrain, LINE_WEBHOOK_DRAIN_INTERVAL_MS);
1246
- drainTimer.unref?.();
1293
+ if (!stopTask) monitor.start();
1247
1294
  },
1248
- stop: async () => {
1249
- if (!running && shutdown.signal.aborted) return;
1250
- running = false;
1251
- if (drainTimer) {
1252
- clearInterval(drainTimer);
1253
- drainTimer = void 0;
1254
- }
1255
- shutdown.abort();
1256
- await drainTask;
1257
- await Promise.allSettled(activeDeliveries);
1258
- await Promise.allSettled(deferredClaims.values());
1259
- await drain.waitForIdle();
1260
- drain.dispose();
1295
+ stop: () => {
1296
+ stopTask ??= (async () => {
1297
+ await monitor.pause();
1298
+ try {
1299
+ const deliveriesSettled = await waitForActiveDeliveriesBeforeDispose(activeDeliveries);
1300
+ if (!deliveriesSettled) options.runtime.log(warn(`line: timed out after ${LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS}ms waiting for active webhook deliveries; releasing drain ownership`));
1301
+ while (deferredClaims.size > 0) await Promise.allSettled(deferredClaims.values());
1302
+ acceptsDeferredClaims = false;
1303
+ if (deliveriesSettled) await monitor.waitForIdle();
1304
+ } finally {
1305
+ await monitor.stop();
1306
+ }
1307
+ })();
1308
+ return stopTask;
1261
1309
  }
1262
1310
  };
1263
1311
  }
@@ -1311,56 +1359,6 @@ function resolveLineDurableReplyOptions(params) {
1311
1359
  return { to: params.to };
1312
1360
  }
1313
1361
  //#endregion
1314
- //#region extensions/line/src/reply-chunks.ts
1315
- async function sendLineReplyChunks(params) {
1316
- const quickReplies = params.quickReplies?.length ? params.quickReplies : void 0;
1317
- let replyTokenUsed = Boolean(params.replyTokenUsed);
1318
- if (params.chunks.length === 0) return { replyTokenUsed };
1319
- if (params.replyToken && !replyTokenUsed) {
1320
- let replySucceeded = false;
1321
- try {
1322
- const replyBatch = params.chunks.slice(0, 5);
1323
- const remaining = params.chunks.slice(replyBatch.length);
1324
- const replyMessages = replyBatch.map((chunk) => ({
1325
- type: "text",
1326
- text: chunk
1327
- }));
1328
- if (quickReplies && remaining.length === 0 && replyMessages.length > 0) {
1329
- const lastIndex = replyMessages.length - 1;
1330
- replyMessages[lastIndex] = params.createTextMessageWithQuickReplies(expectDefined(replyBatch[lastIndex], "last non-empty LINE reply batch chunk"), quickReplies);
1331
- }
1332
- await params.replyMessageLine(params.replyToken, replyMessages, {
1333
- cfg: params.cfg,
1334
- accountId: params.accountId
1335
- });
1336
- replyTokenUsed = true;
1337
- replySucceeded = true;
1338
- for (const [i, chunk] of remaining.entries()) if (i === remaining.length - 1 && quickReplies) await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, {
1339
- cfg: params.cfg,
1340
- accountId: params.accountId
1341
- });
1342
- else await params.pushMessageLine(params.to, chunk, {
1343
- cfg: params.cfg,
1344
- accountId: params.accountId
1345
- });
1346
- return { replyTokenUsed };
1347
- } catch (err) {
1348
- if (replySucceeded) throw err;
1349
- params.onReplyError?.(err);
1350
- replyTokenUsed = true;
1351
- }
1352
- }
1353
- for (const [i, chunk] of params.chunks.entries()) if (i === params.chunks.length - 1 && quickReplies) await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, {
1354
- cfg: params.cfg,
1355
- accountId: params.accountId
1356
- });
1357
- else await params.pushMessageLine(params.to, chunk, {
1358
- cfg: params.cfg,
1359
- accountId: params.accountId
1360
- });
1361
- return { replyTokenUsed };
1362
- }
1363
- //#endregion
1364
1362
  //#region extensions/line/src/signature.ts
1365
1363
  function validateLineSignature(body, signature, channelSecret) {
1366
1364
  return safeEqualSecret(signature, crypto.createHmac("SHA256", channelSecret).update(body).digest("base64"));
@@ -1576,12 +1574,8 @@ async function monitorLineProvider(opts) {
1576
1574
  buildTemplateMessageFromPayload,
1577
1575
  processLineMessage,
1578
1576
  chunkMarkdownText,
1579
- sendLineReplyChunks,
1580
1577
  replyMessageLine,
1581
- pushMessageLine,
1582
- pushTextMessageWithQuickReplies,
1583
1578
  createQuickReplyItems,
1584
- createTextMessageWithQuickReplies,
1585
1579
  pushMessagesLine,
1586
1580
  createFlexMessage,
1587
1581
  buildMediaMessage: buildLineMediaMessage,