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

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 (107) hide show
  1. package/dist/api.js +4 -3
  2. package/dist/basic-cards-Rb-Ow7LN.js +307 -0
  3. package/dist/{card-command-DQ-epsgt.js → card-command-Ch0TwcDW.js} +2 -1
  4. package/dist/{channel-uT5RTtpi.js → channel-CNcv74n2.js} +11 -7
  5. package/dist/channel-plugin-api.js +1 -1
  6. package/dist/channel.runtime-BBCZvRa_.js +4 -0
  7. package/dist/{flex-templates-K1lS9i8L.js → flex-templates-KiKaVYgi.js} +1 -318
  8. package/dist/index.js +1 -1
  9. package/dist/{markdown-to-line-D1vapcdP.js → markdown-to-line-DWIdzBfy.js} +213 -218
  10. package/dist/{monitor-Bk-lpoOZ.js → monitor-BinAXvIY.js} +276 -270
  11. package/dist/monitor.runtime-DKj4FHkA.js +2 -0
  12. package/dist/outbound.runtime-DRyoVa81.js +2 -0
  13. package/dist/quick-reply-fallback-D86FwAfr.js +56 -0
  14. package/dist/{reply-payload-transform-r9sa-TYT.js → reply-payload-transform-d57WYHFm.js} +3 -164
  15. package/dist/runtime-api.js +7 -5
  16. package/dist/send-receipt-CY2ey8XH.js +107 -0
  17. package/dist/setup-api.js +1 -1
  18. package/dist/{setup-surface-BEt4mazl.js → setup-surface-Bili5gWy.js} +62 -4
  19. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupCreateRoute.d.ts.map +1 -1
  20. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupFailedType.d.ts.map +1 -1
  21. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupJobFailedType.d.ts.map +1 -1
  22. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupJobStatus.d.ts.map +1 -1
  23. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupStatus.d.ts.map +1 -1
  24. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexBoxBorderWidth.d.ts.map +1 -1
  25. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexBoxCornerRadius.d.ts.map +1 -1
  26. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexBubble.d.ts.map +1 -1
  27. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexIconSize.d.ts.map +1 -1
  28. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexSpanSize.d.ts.map +1 -1
  29. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexTextFontSize.d.ts.map +1 -1
  30. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/numberOfMessagesResponse.d.ts.map +1 -1
  31. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/postbackAction.d.ts.map +1 -1
  32. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/subscriptionPeriodDemographic.d.ts.map +1 -1
  33. package/node_modules/@line/bot-sdk/dist/cjs/version.d.ts +1 -1
  34. package/node_modules/@line/bot-sdk/dist/cjs/version.js +1 -1
  35. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/event.d.ts +2 -1
  36. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/event.d.ts.map +1 -1
  37. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.d.ts +24 -0
  38. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.d.ts.map +1 -0
  39. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.js +14 -0
  40. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.js.map +1 -0
  41. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.d.ts +1 -0
  42. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.d.ts.map +1 -1
  43. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.js +1 -0
  44. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.js.map +1 -1
  45. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupCreateRoute.d.ts.map +1 -1
  46. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupFailedType.d.ts.map +1 -1
  47. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupJobFailedType.d.ts.map +1 -1
  48. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupJobStatus.d.ts.map +1 -1
  49. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupStatus.d.ts.map +1 -1
  50. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexBoxBorderWidth.d.ts.map +1 -1
  51. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexBoxCornerRadius.d.ts.map +1 -1
  52. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexBubble.d.ts.map +1 -1
  53. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexIconSize.d.ts.map +1 -1
  54. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexSpanSize.d.ts.map +1 -1
  55. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexTextFontSize.d.ts.map +1 -1
  56. package/node_modules/@line/bot-sdk/dist/messaging-api/model/numberOfMessagesResponse.d.ts.map +1 -1
  57. package/node_modules/@line/bot-sdk/dist/messaging-api/model/postbackAction.d.ts.map +1 -1
  58. package/node_modules/@line/bot-sdk/dist/messaging-api/model/subscriptionPeriodDemographic.d.ts.map +1 -1
  59. package/node_modules/@line/bot-sdk/dist/version.d.ts +1 -1
  60. package/node_modules/@line/bot-sdk/dist/version.js +1 -1
  61. package/node_modules/@line/bot-sdk/dist/webhook/model/event.d.ts +2 -1
  62. package/node_modules/@line/bot-sdk/dist/webhook/model/event.d.ts.map +1 -1
  63. package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.d.ts +24 -0
  64. package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.d.ts.map +1 -0
  65. package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.js +13 -0
  66. package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.js.map +1 -0
  67. package/node_modules/@line/bot-sdk/dist/webhook/model/models.d.ts +1 -0
  68. package/node_modules/@line/bot-sdk/dist/webhook/model/models.d.ts.map +1 -1
  69. package/node_modules/@line/bot-sdk/dist/webhook/model/models.js +1 -0
  70. package/node_modules/@line/bot-sdk/dist/webhook/model/models.js.map +1 -1
  71. package/node_modules/@line/bot-sdk/lib/insight/model/getNumberOfFollowersResponse.ts +1 -1
  72. package/node_modules/@line/bot-sdk/lib/liff/model/liffScope.ts +1 -1
  73. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupCreateRoute.ts +1 -4
  74. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupFailedType.ts +1 -2
  75. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupJobFailedType.ts +1 -2
  76. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupJobStatus.ts +1 -4
  77. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupStatus.ts +1 -6
  78. package/node_modules/@line/bot-sdk/lib/messaging-api/model/botInfoResponse.ts +1 -1
  79. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexBoxBorderWidth.ts +1 -6
  80. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexBoxCornerRadius.ts +1 -7
  81. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexBubble.ts +1 -7
  82. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexIconSize.ts +1 -10
  83. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexSpanSize.ts +1 -10
  84. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexTextFontSize.ts +1 -10
  85. package/node_modules/@line/bot-sdk/lib/messaging-api/model/getAggregationUnitNameListResponse.ts +1 -1
  86. package/node_modules/@line/bot-sdk/lib/messaging-api/model/getFollowersResponse.ts +1 -1
  87. package/node_modules/@line/bot-sdk/lib/messaging-api/model/getJoinedMembershipUsersResponse.ts +1 -1
  88. package/node_modules/@line/bot-sdk/lib/messaging-api/model/issueLinkTokenResponse.ts +1 -1
  89. package/node_modules/@line/bot-sdk/lib/messaging-api/model/numberOfMessagesResponse.ts +2 -5
  90. package/node_modules/@line/bot-sdk/lib/messaging-api/model/postbackAction.ts +1 -4
  91. package/node_modules/@line/bot-sdk/lib/messaging-api/model/subscriptionPeriodDemographic.ts +1 -5
  92. package/node_modules/@line/bot-sdk/lib/version.ts +1 -1
  93. package/node_modules/@line/bot-sdk/lib/webhook/.openapi-generator/FILES +1 -0
  94. package/node_modules/@line/bot-sdk/lib/webhook/model/event.ts +2 -0
  95. package/node_modules/@line/bot-sdk/lib/webhook/model/messageEditedEvent.ts +32 -0
  96. package/node_modules/@line/bot-sdk/lib/webhook/model/models.ts +1 -0
  97. package/node_modules/@line/bot-sdk/lib/webhook/model/stickerMessageContent.ts +1 -1
  98. package/node_modules/@line/bot-sdk/package.json +3 -3
  99. package/node_modules/@types/node/README.md +1 -1
  100. package/node_modules/@types/node/fs/promises.d.ts +16 -5
  101. package/node_modules/@types/node/package.json +3 -3
  102. package/npm-shrinkwrap.json +10 -10
  103. package/openclaw.plugin.json +0 -18
  104. package/package.json +71 -6
  105. package/dist/channel.runtime-BqNglqnr.js +0 -4
  106. package/dist/monitor.runtime-CHlqxGVL.js +0 -2
  107. package/dist/outbound.runtime-ByZ0Dsvu.js +0 -2
@@ -1,22 +1,23 @@
1
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";
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-DWIdzBfy.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 { 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";
@@ -52,44 +53,85 @@ const CONTENT_READY_BASE_DELAY_MS = 500;
52
53
  const CONTENT_READY_MAX_DELAY_MS = 4e3;
53
54
  const CONTENT_READY_TIMEOUT_MS = 15e3;
54
55
  const LINE_CONTENT_BASE_URL = "https://api-data.line.me/v2/bot/message";
56
+ var RetryableLineMediaFetchError = class extends MediaFetchError {
57
+ constructor(message, options) {
58
+ super("fetch_failed", message, options);
59
+ this.name = "RetryableLineMediaFetchError";
60
+ }
61
+ };
55
62
  function contentBackoffDelayMs(attempt) {
56
63
  return Math.min(CONTENT_READY_BASE_DELAY_MS * 2 ** attempt, CONTENT_READY_MAX_DELAY_MS);
57
64
  }
65
+ function lineContentUrl(messageId) {
66
+ return `${LINE_CONTENT_BASE_URL}/${encodeURIComponent(messageId)}/content`;
67
+ }
68
+ async function* lineResponseBodyChunks(response, messageId) {
69
+ const body = response.body;
70
+ if (!body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
71
+ const reader = body.getReader();
72
+ let completed = false;
73
+ try {
74
+ while (true) {
75
+ let chunk;
76
+ try {
77
+ chunk = await reader.read();
78
+ } catch (err) {
79
+ throw new RetryableLineMediaFetchError(`LINE media response stream failed for message ${messageId}`, { cause: err });
80
+ }
81
+ if (chunk.done) {
82
+ completed = true;
83
+ return;
84
+ }
85
+ if (chunk.value.byteLength > 0) yield chunk.value;
86
+ }
87
+ } finally {
88
+ if (!completed) await reader.cancel().catch(() => void 0);
89
+ try {
90
+ reader.releaseLock();
91
+ } catch {}
92
+ }
93
+ }
58
94
  async function fetchLineContentWhenReady(messageId, channelAccessToken) {
59
95
  const controller = new AbortController();
60
96
  const deadline = setTimeout(() => controller.abort(), CONTENT_READY_TIMEOUT_MS);
61
97
  deadline.unref();
62
98
  try {
63
99
  for (let attempt = 0; attempt < CONTENT_READY_MAX_ATTEMPTS; attempt++) {
64
- const response = await fetchWithRuntimeDispatcherOrMockedGlobal(`${LINE_CONTENT_BASE_URL}/${encodeURIComponent(messageId)}/content`, {
100
+ const response = await fetchWithRuntimeDispatcherOrMockedGlobal(lineContentUrl(messageId), {
65
101
  headers: { Authorization: `Bearer ${channelAccessToken}` },
66
102
  redirect: "error",
67
103
  signal: controller.signal
68
104
  });
69
105
  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);
106
+ if (!response.body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
107
+ return response;
72
108
  }
73
- await response.body?.cancel();
74
- if (response.status !== 202) throw new Error(`LINE media download failed for message ${messageId} (HTTP ${response.status})`);
109
+ await response.body?.cancel().catch(() => void 0);
110
+ if (response.status !== 202) throw new MediaFetchError("http_error", `LINE media download failed for message ${messageId} (HTTP ${response.status})`, { status: response.status });
75
111
  if (attempt < CONTENT_READY_MAX_ATTEMPTS - 1) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal: controller.signal });
76
112
  }
77
113
  } 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;
114
+ 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 });
115
+ if (err instanceof MediaFetchError) throw err;
116
+ throw new RetryableLineMediaFetchError(`LINE media download failed for message ${messageId}`, { cause: err });
80
117
  } finally {
81
118
  clearTimeout(deadline);
82
119
  }
83
- throw new Error(`LINE media for message ${messageId} was still preparing (HTTP 202) after ${CONTENT_READY_MAX_ATTEMPTS} attempts`);
120
+ throw new MediaFetchError("http_error", `LINE media for message ${messageId} was still preparing (HTTP 202) after ${CONTENT_READY_MAX_ATTEMPTS} attempts`, { status: 202 });
121
+ }
122
+ function isRetryableLineInboundMediaError(err) {
123
+ if (err instanceof RetryableLineMediaFetchError) return true;
124
+ if (!(err instanceof MediaFetchError)) return false;
125
+ if (err.code === "http_error") return err.status === 202 || err.status === 408 || err.status === 429 || typeof err.status === "number" && err.status >= 500;
126
+ return false;
84
127
  }
85
128
  async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 * 1024 * 1024, options) {
86
- const content = await fetchLineContentWhenReady(messageId, channelAccessToken);
129
+ const response = await fetchLineContentWhenReady(messageId, channelAccessToken);
87
130
  let saved;
88
131
  try {
89
- saved = await saveMediaStream(content, void 0, "inbound", maxBytes, options?.originalFilename);
132
+ saved = await saveMediaStream(lineResponseBodyChunks(response, messageId), response.headers.get("content-type") ?? void 0, "inbound", maxBytes, options?.originalFilename);
90
133
  } catch (err) {
91
- content.destroy();
92
- await finished(content).catch(() => void 0);
134
+ await response.body?.cancel().catch(() => void 0);
93
135
  throw err;
94
136
  }
95
137
  logVerbose(`line: persisted media ${messageId} to ${saved.path} (${saved.size} bytes)`);
@@ -102,7 +144,11 @@ async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 *
102
144
  //#endregion
103
145
  //#region extensions/line/src/auto-reply-delivery.ts
104
146
  function toLineDeliveryError(error) {
105
- return error instanceof Error ? error : new Error("LINE rich or media message send failed", { cause: error });
147
+ return error instanceof Error ? error : new Error("LINE message send failed", { cause: error });
148
+ }
149
+ function getLineHttpError(error) {
150
+ if (error instanceof HTTPFetchError) return error;
151
+ return error instanceof Error && error.cause instanceof HTTPFetchError ? error.cause : void 0;
106
152
  }
107
153
  function markLineVisibleDeliveryError(error) {
108
154
  const deliveryError = toLineDeliveryError(error);
@@ -113,7 +159,7 @@ function markLineVisibleDeliveryError(error) {
113
159
  });
114
160
  return deliveryError;
115
161
  }
116
- const visibleError = new Error("LINE rich or media message send failed", { cause: deliveryError });
162
+ const visibleError = new Error("LINE message send failed", { cause: deliveryError });
117
163
  Object.assign(visibleError, {
118
164
  sentBeforeError: true,
119
165
  visibleReplySent: true
@@ -135,14 +181,25 @@ async function deliverLineAutoReply(params) {
135
181
  }
136
182
  };
137
183
  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) => {
184
+ const failedPushSegments = /* @__PURE__ */ new WeakMap();
185
+ const pushLineMessages = async (messages, allowFailedBatchTextRecovery, externalTail = []) => {
141
186
  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
- }));
187
+ for (let i = 0; i < messages.length; i += 5) {
188
+ const batch = messages.slice(i, i + 5);
189
+ try {
190
+ await sendVisible(() => deps.pushMessagesLine(to, batch, {
191
+ cfg: params.cfg,
192
+ accountId
193
+ }));
194
+ } catch (error) {
195
+ if (typeof error === "object" && error !== null) failedPushSegments.set(error, {
196
+ allowFailedBatchTextRecovery,
197
+ failedBatch: batch,
198
+ unattemptedTail: [...messages.slice(i + batch.length), ...externalTail]
199
+ });
200
+ throw error;
201
+ }
202
+ }
146
203
  };
147
204
  const sendLineMessages = async (messages, allowReplyToken) => {
148
205
  if (messages.length === 0) return;
@@ -156,12 +213,13 @@ async function deliverLineAutoReply(params) {
156
213
  });
157
214
  } catch (err) {
158
215
  deps.onReplyError?.(err);
159
- await pushLineMessages(replyBatch);
216
+ await pushLineMessages(replyBatch, getLineHttpError(err)?.status === 400, remaining.slice(replyBatch.length));
217
+ } finally {
218
+ replyTokenUsed = true;
160
219
  }
161
- replyTokenUsed = true;
162
220
  remaining = remaining.slice(replyBatch.length);
163
221
  }
164
- if (remaining.length > 0) await pushLineMessages(remaining);
222
+ if (remaining.length > 0) await pushLineMessages(remaining, true);
165
223
  };
166
224
  const richMessages = [];
167
225
  const hasQuickReplies = Boolean(lineData.quickReplies?.length);
@@ -186,82 +244,59 @@ async function deliverLineAutoReply(params) {
186
244
  trackingId: lineData.trackingId
187
245
  };
188
246
  const mediaMessages = [];
189
- let richMediaError;
247
+ let deliveryError;
190
248
  for (const rawUrl of mediaUrls) {
191
249
  const url = rawUrl?.trim();
192
250
  if (!url) continue;
193
251
  try {
194
252
  mediaMessages.push(await deps.buildMediaMessage(url, mediaOpts, to));
195
253
  } catch (err) {
196
- richMediaError ??= err;
254
+ deliveryError ??= err;
197
255
  }
198
256
  }
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
- }
257
+ const textMessages = chunks.map((text) => ({
258
+ type: "text",
259
+ text
260
+ }));
261
+ const richMediaMessages = [...richMessages, ...mediaMessages];
262
+ if (hasQuickReplies && textMessages.length === 0 && richMediaMessages.length === 0) textMessages.push({
263
+ type: "text",
264
+ text: buildLineQuickReplyFallbackText(lineData.quickReplies)
265
+ });
266
+ if (hasQuickReplies) {
267
+ const targetMessages = textMessages.length > 0 ? textMessages : richMediaMessages;
268
+ const lastIndex = targetMessages.length - 1;
269
+ targetMessages[lastIndex] = {
270
+ ...expectDefined(targetMessages[lastIndex], "last LINE auto-reply message"),
271
+ quickReply: deps.createQuickReplyItems(lineData.quickReplies)
272
+ };
257
273
  }
258
- if (richMediaError !== void 0) {
259
- if (!visibleReplySent) throw toLineDeliveryError(richMediaError);
274
+ const messages = hasQuickReplies ? [...richMediaMessages, ...textMessages] : [...textMessages, ...richMediaMessages];
275
+ try {
276
+ await sendLineMessages(messages, true);
277
+ } catch (err) {
278
+ deliveryError ??= err;
279
+ const failedSegment = typeof err === "object" && err !== null ? failedPushSegments.get(err) : void 0;
280
+ const httpError = getLineHttpError(err);
281
+ const retryCandidates = failedSegment ? [...failedSegment.allowFailedBatchTextRecovery ? failedSegment.failedBatch : [], ...failedSegment.unattemptedTail] : [];
282
+ const retryTextMessages = retryCandidates.filter((message) => message.type === "text");
283
+ const quickRepliesNeedCarrier = hasQuickReplies && retryCandidates.some((message) => "quickReply" in message);
284
+ const retryMessages = retryTextMessages.length > 0 ? retryTextMessages : quickRepliesNeedCarrier ? [{
285
+ type: "text",
286
+ text: buildLineQuickReplyFallbackText(lineData.quickReplies),
287
+ quickReply: deps.createQuickReplyItems(lineData.quickReplies)
288
+ }] : [];
289
+ if (retryMessages.length > 0 && failedSegment?.failedBatch.some((message) => message.type !== "text") && httpError?.status === 400) try {
290
+ await sendLineMessages(retryMessages, false);
291
+ } catch {}
292
+ }
293
+ if (deliveryError !== void 0) {
294
+ if (!visibleReplySent) throw toLineDeliveryError(deliveryError);
260
295
  return {
261
296
  status: "partial",
262
297
  replyTokenUsed,
263
298
  visibleReplySent: true,
264
- error: markLineVisibleDeliveryError(richMediaError)
299
+ error: markLineVisibleDeliveryError(deliveryError)
265
300
  };
266
301
  }
267
302
  return {
@@ -397,13 +432,13 @@ function extractMessageText(message) {
397
432
  }
398
433
  return "";
399
434
  }
400
- function extractMediaPlaceholder(message) {
435
+ function extractNativeMediaKind(message) {
401
436
  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 "";
437
+ case "image": return "image";
438
+ case "video": return "video";
439
+ case "audio": return "audio";
440
+ case "file": return "document";
441
+ default: return;
407
442
  }
408
443
  }
409
444
  function resolveLineConversationLabel(params) {
@@ -439,6 +474,7 @@ async function finalizeLineInboundContext(params) {
439
474
  sessionKey: params.route.sessionKey
440
475
  });
441
476
  const agentBody = params.agentBody ?? params.rawBody;
477
+ const media = toInboundMediaFacts(params.media);
442
478
  const body = formatInboundEnvelope({
443
479
  channel: "LINE",
444
480
  from: conversationLabel,
@@ -467,12 +503,7 @@ async function finalizeLineInboundContext(params) {
467
503
  Surface: "line",
468
504
  MessageSid: params.messageSid,
469
505
  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,
506
+ media,
476
507
  ...params.locationContext,
477
508
  CommandAuthorized: params.commandAuthorized,
478
509
  OriginatingChannel: "line",
@@ -535,15 +566,17 @@ async function buildLineMessageContext(params) {
535
566
  const messageId = message.id;
536
567
  const timestamp = event.timestamp;
537
568
  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)` : ""}`;
569
+ const nativeMediaKind = extractNativeMediaKind(message);
570
+ const mediaFacts = allMedia.length > 0 ? allMedia.map((media) => ({
571
+ ...media,
572
+ kind: nativeMediaKind
573
+ })) : nativeMediaKind ? [{ kind: nativeMediaKind }] : [];
574
+ const rawBody = textContent;
541
575
  const agentBody = mediaUnavailable ? formatInboundMediaUnavailableText({
542
576
  body: rawBody,
543
- mediaPlaceholder: placeholder,
544
577
  notice: "[line attachment unavailable]"
545
578
  }) : rawBody;
546
- if (!agentBody && allMedia.length === 0) return null;
579
+ if (!agentBody && mediaFacts.length === 0) return null;
547
580
  let locationContext;
548
581
  if (message.type === "location") {
549
582
  const loc = message;
@@ -571,12 +604,7 @@ async function buildLineMessageContext(params) {
571
604
  timestamp,
572
605
  messageSid: messageId,
573
606
  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
- },
607
+ media: mediaFacts,
580
608
  locationContext,
581
609
  verboseLog: {
582
610
  kind: "inbound",
@@ -632,12 +660,7 @@ async function buildLinePostbackContext(params) {
632
660
  timestamp,
633
661
  messageSid,
634
662
  commandAuthorized,
635
- media: {
636
- firstPath: "",
637
- firstContentType: void 0,
638
- paths: void 0,
639
- types: void 0
640
- },
663
+ media: [],
641
664
  verboseLog: { kind: "postback" }
642
665
  });
643
666
  return {
@@ -952,6 +975,7 @@ async function handleMessageEvent(event, context) {
952
975
  contentType: media.contentType
953
976
  });
954
977
  } catch (err) {
978
+ if (isRetryableLineInboundMediaError(err)) throw err;
955
979
  mediaUnavailable = true;
956
980
  const errMsg = String(err);
957
981
  if (errMsg.includes("exceeds") && errMsg.includes("limit")) logVerbose(`line: media exceeds size limit for message ${message.id}`);
@@ -1052,6 +1076,7 @@ const LINE_WEBHOOK_SPOOL_VERSION = 1;
1052
1076
  const LINE_WEBHOOK_DRAIN_INTERVAL_MS = 500;
1053
1077
  const LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
1054
1078
  const LINE_WEBHOOK_DRAIN_SCAN_LIMIT = 100;
1079
+ const LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS = 5e3;
1055
1080
  const LINE_WEBHOOK_TOMBSTONE_TTL_MS = 720 * 60 * 6e4;
1056
1081
  const LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES = 4096;
1057
1082
  var LineWebhookPayloadError = class extends Error {
@@ -1099,15 +1124,13 @@ function laneKeyFor(event, eventId) {
1099
1124
  }
1100
1125
  return eventId;
1101
1126
  }
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.");
1127
+ function parseStoredEvent(rawEvent) {
1104
1128
  let event;
1105
1129
  try {
1106
- event = JSON.parse(payload.rawEvent);
1130
+ event = JSON.parse(rawEvent);
1107
1131
  } catch (error) {
1108
1132
  throw new LineWebhookPayloadError("LINE webhook event JSON is invalid.", { cause: error });
1109
1133
  }
1110
- if (eventIdFor(event) !== claimedId) throw new LineWebhookPayloadError("LINE webhook event id changed after durable admission.");
1111
1134
  return event;
1112
1135
  }
1113
1136
  function errorText(error) {
@@ -1118,41 +1141,61 @@ function isLineAuthenticationFailure(error) {
1118
1141
  const status = error.status;
1119
1142
  return status === 401 || status === 403;
1120
1143
  }
1144
+ async function waitForActiveDeliveriesBeforeDispose(activeDeliveries) {
1145
+ let timeout;
1146
+ try {
1147
+ return await Promise.race([Promise.allSettled(activeDeliveries).then(() => true), new Promise((resolve) => {
1148
+ timeout = setTimeout(() => resolve(false), LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS);
1149
+ timeout.unref?.();
1150
+ })]);
1151
+ } finally {
1152
+ if (timeout) clearTimeout(timeout);
1153
+ }
1154
+ }
1121
1155
  function createLineWebhookSpool(options) {
1122
1156
  const queue = options.queue ?? getLineRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
1123
- const shutdown = new AbortController();
1124
1157
  const activeDeliveries = /* @__PURE__ */ new Set();
1125
1158
  const deferredClaims = /* @__PURE__ */ new Map();
1126
- const drain = createChannelIngressDrain({
1159
+ let acceptsDeferredClaims = true;
1160
+ const monitor = createChannelIngressMonitor({
1127
1161
  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
1162
+ inspect: ({ event }) => {
1163
+ const eventId = eventIdFor(event);
1164
+ return {
1165
+ eventId,
1166
+ laneKey: laneKeyFor(event, eventId)
1141
1167
  };
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
1168
  },
1152
- onLog: (message) => options.runtime.error?.(danger(`line: ${message}`)),
1153
- dispatchClaimedEvent: async (claimed, lifecycle) => {
1154
- const event = parseClaimedEvent(claimed.payload, claimed.id);
1169
+ payload: {
1170
+ version: LINE_WEBHOOK_SPOOL_VERSION,
1171
+ serialize: ({ event, destination }) => ({
1172
+ rawEvent: JSON.stringify(event),
1173
+ destination
1174
+ }),
1175
+ deserialize: ({ rawEvent, destination }) => ({
1176
+ event: parseStoredEvent(rawEvent),
1177
+ destination
1178
+ }),
1179
+ encode: ({ version, body }) => ({
1180
+ version,
1181
+ rawEvent: body.rawEvent,
1182
+ destination: body.destination
1183
+ }),
1184
+ decode: (payload) => {
1185
+ if (typeof payload.rawEvent !== "string" || typeof payload.destination !== "string") throw new LineWebhookPayloadError("LINE webhook spool payload is invalid.");
1186
+ return {
1187
+ version: payload.version,
1188
+ body: {
1189
+ rawEvent: payload.rawEvent,
1190
+ destination: payload.destination
1191
+ }
1192
+ };
1193
+ },
1194
+ createClaimError: (kind) => new LineWebhookPayloadError(kind === "invalid-version" ? "LINE webhook spool payload is invalid." : "LINE webhook event identity changed after durable admission.")
1195
+ },
1196
+ deliver: async ({ event, destination }, lifecycle, claim) => {
1155
1197
  const boundLifecycle = bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle;
1198
+ let handedOff = false;
1156
1199
  let resolveDeferredClaim;
1157
1200
  const deferredClaim = new Promise((resolve) => {
1158
1201
  resolveDeferredClaim = resolve;
@@ -1161,12 +1204,13 @@ function createLineWebhookSpool(options) {
1161
1204
  const settleDeferredClaim = () => {
1162
1205
  if (deferredClaimSettled) return;
1163
1206
  deferredClaimSettled = true;
1164
- if (deferredClaims.get(claimed.id) === deferredClaim) deferredClaims.delete(claimed.id);
1207
+ if (deferredClaims.get(claim.id) === deferredClaim) deferredClaims.delete(claim.id);
1165
1208
  resolveDeferredClaim();
1166
1209
  };
1167
- const delivery = options.deliver(event, claimed.payload.destination, { turnAdoptionLifecycle: {
1210
+ const delivery = options.deliver(event, destination, { turnAdoptionLifecycle: {
1168
1211
  ...boundLifecycle,
1169
1212
  onAdopted: async () => {
1213
+ handedOff = true;
1170
1214
  try {
1171
1215
  await boundLifecycle.onAdopted();
1172
1216
  } finally {
@@ -1174,10 +1218,19 @@ function createLineWebhookSpool(options) {
1174
1218
  }
1175
1219
  },
1176
1220
  onDeferred: () => {
1177
- if (!deferredClaimSettled) deferredClaims.set(claimed.id, deferredClaim);
1221
+ handedOff = true;
1222
+ if (!acceptsDeferredClaims) {
1223
+ settleDeferredClaim();
1224
+ Promise.resolve().then(() => boundLifecycle.onAbandoned()).catch((error) => {
1225
+ options.runtime.error?.(danger(`line: failed to abandon a late webhook delivery: ${errorText(error)}`));
1226
+ });
1227
+ return;
1228
+ }
1229
+ if (!deferredClaimSettled) deferredClaims.set(claim.id, deferredClaim);
1178
1230
  boundLifecycle.onDeferred();
1179
1231
  },
1180
1232
  onAbandoned: async () => {
1233
+ handedOff = true;
1181
1234
  try {
1182
1235
  await boundLifecycle.onAbandoned();
1183
1236
  } finally {
@@ -1191,73 +1244,80 @@ function createLineWebhookSpool(options) {
1191
1244
  } finally {
1192
1245
  activeDeliveries.delete(delivery);
1193
1246
  }
1194
- }
1247
+ if (stopTask && !handedOff) return {
1248
+ kind: "failed-retryable",
1249
+ error: /* @__PURE__ */ new Error("LINE webhook spool stopped before delivery handoff.")
1250
+ };
1251
+ },
1252
+ pollIntervalMs: LINE_WEBHOOK_DRAIN_INTERVAL_MS,
1253
+ retention: {
1254
+ pruneIntervalMs: 0,
1255
+ completedTtlMs: LINE_WEBHOOK_TOMBSTONE_TTL_MS,
1256
+ completedMaxEntries: LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES,
1257
+ failedTtlMs: LINE_WEBHOOK_TOMBSTONE_TTL_MS,
1258
+ failedMaxEntries: LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES
1259
+ },
1260
+ appendRetryDelaysMs: [0],
1261
+ waitForDeliveryIdleBeforeRepump: false,
1262
+ waitForDeliveryIdleOnStop: false,
1263
+ runPumpTask: runDetachedWebhookWork,
1264
+ admissionMode: "durable-after-stop",
1265
+ drain: {
1266
+ adoptionStallTimeoutMs: DEFAULT_INGRESS_ADOPTION_STALL_MS,
1267
+ orderBy: "received",
1268
+ scanLimit: LINE_WEBHOOK_DRAIN_SCAN_LIMIT,
1269
+ startLimit: LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES,
1270
+ retryPolicy: {
1271
+ maxAttempts: DEFAULT_INGRESS_RETRY_MAX_ATTEMPTS,
1272
+ deadLetterMinAgeMs: 0
1273
+ },
1274
+ resolveNonRetryableFailure: (error) => {
1275
+ if (error instanceof LineWebhookPayloadError) return {
1276
+ reason: "invalid-event",
1277
+ message: error.message
1278
+ };
1279
+ if (error instanceof LineWebhookTerminalDeliveryError) return {
1280
+ reason: error.reason,
1281
+ message: error.message
1282
+ };
1283
+ if (isLineAuthenticationFailure(error)) return {
1284
+ reason: "authentication-failed",
1285
+ message: errorText(error)
1286
+ };
1287
+ return null;
1288
+ },
1289
+ onLog: (message) => options.runtime.error?.(danger(`line: ${message}`))
1290
+ },
1291
+ createStoppedError: () => /* @__PURE__ */ new Error("LINE webhook spool is stopped."),
1292
+ onError: (error) => options.runtime.error?.(danger(`line: webhook spool drain failed: ${errorText(error)}`))
1195
1293
  });
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
- };
1294
+ let stopTask;
1217
1295
  return {
1218
1296
  accept: async (body) => {
1219
1297
  const events = body.events ?? [];
1220
1298
  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();
1299
+ await monitor.admitBatch(events.map((event) => ({
1300
+ event,
1301
+ destination: body.destination ?? ""
1302
+ })), { receivedAt: Date.now() });
1240
1303
  },
1241
1304
  start: () => {
1242
- if (running) return;
1243
- running = true;
1244
- requestDrain();
1245
- drainTimer = setInterval(requestDrain, LINE_WEBHOOK_DRAIN_INTERVAL_MS);
1246
- drainTimer.unref?.();
1305
+ if (!stopTask) monitor.start();
1247
1306
  },
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();
1307
+ stop: () => {
1308
+ stopTask ??= (async () => {
1309
+ await monitor.pause();
1310
+ try {
1311
+ const deliveriesSettled = await waitForActiveDeliveriesBeforeDispose(activeDeliveries);
1312
+ 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`));
1313
+ while (deferredClaims.size > 0) await Promise.allSettled(deferredClaims.values());
1314
+ acceptsDeferredClaims = false;
1315
+ if (deliveriesSettled) await monitor.waitForIdle();
1316
+ } finally {
1317
+ await monitor.stop();
1318
+ }
1319
+ })();
1320
+ return stopTask;
1261
1321
  }
1262
1322
  };
1263
1323
  }
@@ -1311,56 +1371,6 @@ function resolveLineDurableReplyOptions(params) {
1311
1371
  return { to: params.to };
1312
1372
  }
1313
1373
  //#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
1374
  //#region extensions/line/src/signature.ts
1365
1375
  function validateLineSignature(body, signature, channelSecret) {
1366
1376
  return safeEqualSecret(signature, crypto.createHmac("SHA256", channelSecret).update(body).digest("base64"));
@@ -1576,12 +1586,8 @@ async function monitorLineProvider(opts) {
1576
1586
  buildTemplateMessageFromPayload,
1577
1587
  processLineMessage,
1578
1588
  chunkMarkdownText,
1579
- sendLineReplyChunks,
1580
1589
  replyMessageLine,
1581
- pushMessageLine,
1582
- pushTextMessageWithQuickReplies,
1583
1590
  createQuickReplyItems,
1584
- createTextMessageWithQuickReplies,
1585
1591
  pushMessagesLine,
1586
1592
  createFlexMessage,
1587
1593
  buildMediaMessage: buildLineMediaMessage,