@openclaw/line 2026.7.2-beta.2 → 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 (109) hide show
  1. package/dist/{accounts-DJVOv1JI.js → accounts-BUBOoW5Q.js} +95 -33
  2. package/dist/api.js +4 -3
  3. package/dist/basic-cards-Rb-Ow7LN.js +307 -0
  4. package/dist/{card-command-DQ-epsgt.js → card-command-Ch0TwcDW.js} +2 -1
  5. package/dist/{channel-BHR-GoJ0.js → channel-CNcv74n2.js} +22 -10
  6. package/dist/channel-plugin-api.js +1 -1
  7. package/dist/channel.runtime-BBCZvRa_.js +4 -0
  8. package/dist/contract-api.js +1 -1
  9. package/dist/{flex-templates-K1lS9i8L.js → flex-templates-KiKaVYgi.js} +1 -318
  10. package/dist/index.js +1 -1
  11. package/dist/{markdown-to-line-XceqeKRP.js → markdown-to-line-DWIdzBfy.js} +224 -220
  12. package/dist/{monitor-Db2rZsSX.js → monitor-BinAXvIY.js} +454 -288
  13. package/dist/monitor.runtime-DKj4FHkA.js +2 -0
  14. package/dist/outbound.runtime-DRyoVa81.js +2 -0
  15. package/dist/quick-reply-fallback-D86FwAfr.js +56 -0
  16. package/dist/{reply-payload-transform-r9sa-TYT.js → reply-payload-transform-d57WYHFm.js} +3 -164
  17. package/dist/runtime-api.js +10 -24
  18. package/dist/send-receipt-CY2ey8XH.js +107 -0
  19. package/dist/setup-api.js +1 -1
  20. package/dist/{setup-surface-B7W6pwn3.js → setup-surface-Bili5gWy.js} +64 -5
  21. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupCreateRoute.d.ts.map +1 -1
  22. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupFailedType.d.ts.map +1 -1
  23. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupJobFailedType.d.ts.map +1 -1
  24. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupJobStatus.d.ts.map +1 -1
  25. package/node_modules/@line/bot-sdk/dist/cjs/manage-audience/model/audienceGroupStatus.d.ts.map +1 -1
  26. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexBoxBorderWidth.d.ts.map +1 -1
  27. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexBoxCornerRadius.d.ts.map +1 -1
  28. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexBubble.d.ts.map +1 -1
  29. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexIconSize.d.ts.map +1 -1
  30. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexSpanSize.d.ts.map +1 -1
  31. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/flexTextFontSize.d.ts.map +1 -1
  32. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/numberOfMessagesResponse.d.ts.map +1 -1
  33. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/postbackAction.d.ts.map +1 -1
  34. package/node_modules/@line/bot-sdk/dist/cjs/messaging-api/model/subscriptionPeriodDemographic.d.ts.map +1 -1
  35. package/node_modules/@line/bot-sdk/dist/cjs/version.d.ts +1 -1
  36. package/node_modules/@line/bot-sdk/dist/cjs/version.js +1 -1
  37. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/event.d.ts +2 -1
  38. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/event.d.ts.map +1 -1
  39. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.d.ts +24 -0
  40. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.d.ts.map +1 -0
  41. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.js +14 -0
  42. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/messageEditedEvent.js.map +1 -0
  43. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.d.ts +1 -0
  44. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.d.ts.map +1 -1
  45. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.js +1 -0
  46. package/node_modules/@line/bot-sdk/dist/cjs/webhook/model/models.js.map +1 -1
  47. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupCreateRoute.d.ts.map +1 -1
  48. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupFailedType.d.ts.map +1 -1
  49. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupJobFailedType.d.ts.map +1 -1
  50. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupJobStatus.d.ts.map +1 -1
  51. package/node_modules/@line/bot-sdk/dist/manage-audience/model/audienceGroupStatus.d.ts.map +1 -1
  52. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexBoxBorderWidth.d.ts.map +1 -1
  53. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexBoxCornerRadius.d.ts.map +1 -1
  54. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexBubble.d.ts.map +1 -1
  55. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexIconSize.d.ts.map +1 -1
  56. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexSpanSize.d.ts.map +1 -1
  57. package/node_modules/@line/bot-sdk/dist/messaging-api/model/flexTextFontSize.d.ts.map +1 -1
  58. package/node_modules/@line/bot-sdk/dist/messaging-api/model/numberOfMessagesResponse.d.ts.map +1 -1
  59. package/node_modules/@line/bot-sdk/dist/messaging-api/model/postbackAction.d.ts.map +1 -1
  60. package/node_modules/@line/bot-sdk/dist/messaging-api/model/subscriptionPeriodDemographic.d.ts.map +1 -1
  61. package/node_modules/@line/bot-sdk/dist/version.d.ts +1 -1
  62. package/node_modules/@line/bot-sdk/dist/version.js +1 -1
  63. package/node_modules/@line/bot-sdk/dist/webhook/model/event.d.ts +2 -1
  64. package/node_modules/@line/bot-sdk/dist/webhook/model/event.d.ts.map +1 -1
  65. package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.d.ts +24 -0
  66. package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.d.ts.map +1 -0
  67. package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.js +13 -0
  68. package/node_modules/@line/bot-sdk/dist/webhook/model/messageEditedEvent.js.map +1 -0
  69. package/node_modules/@line/bot-sdk/dist/webhook/model/models.d.ts +1 -0
  70. package/node_modules/@line/bot-sdk/dist/webhook/model/models.d.ts.map +1 -1
  71. package/node_modules/@line/bot-sdk/dist/webhook/model/models.js +1 -0
  72. package/node_modules/@line/bot-sdk/dist/webhook/model/models.js.map +1 -1
  73. package/node_modules/@line/bot-sdk/lib/insight/model/getNumberOfFollowersResponse.ts +1 -1
  74. package/node_modules/@line/bot-sdk/lib/liff/model/liffScope.ts +1 -1
  75. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupCreateRoute.ts +1 -4
  76. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupFailedType.ts +1 -2
  77. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupJobFailedType.ts +1 -2
  78. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupJobStatus.ts +1 -4
  79. package/node_modules/@line/bot-sdk/lib/manage-audience/model/audienceGroupStatus.ts +1 -6
  80. package/node_modules/@line/bot-sdk/lib/messaging-api/model/botInfoResponse.ts +1 -1
  81. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexBoxBorderWidth.ts +1 -6
  82. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexBoxCornerRadius.ts +1 -7
  83. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexBubble.ts +1 -7
  84. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexIconSize.ts +1 -10
  85. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexSpanSize.ts +1 -10
  86. package/node_modules/@line/bot-sdk/lib/messaging-api/model/flexTextFontSize.ts +1 -10
  87. package/node_modules/@line/bot-sdk/lib/messaging-api/model/getAggregationUnitNameListResponse.ts +1 -1
  88. package/node_modules/@line/bot-sdk/lib/messaging-api/model/getFollowersResponse.ts +1 -1
  89. package/node_modules/@line/bot-sdk/lib/messaging-api/model/getJoinedMembershipUsersResponse.ts +1 -1
  90. package/node_modules/@line/bot-sdk/lib/messaging-api/model/issueLinkTokenResponse.ts +1 -1
  91. package/node_modules/@line/bot-sdk/lib/messaging-api/model/numberOfMessagesResponse.ts +2 -5
  92. package/node_modules/@line/bot-sdk/lib/messaging-api/model/postbackAction.ts +1 -4
  93. package/node_modules/@line/bot-sdk/lib/messaging-api/model/subscriptionPeriodDemographic.ts +1 -5
  94. package/node_modules/@line/bot-sdk/lib/version.ts +1 -1
  95. package/node_modules/@line/bot-sdk/lib/webhook/.openapi-generator/FILES +1 -0
  96. package/node_modules/@line/bot-sdk/lib/webhook/model/event.ts +2 -0
  97. package/node_modules/@line/bot-sdk/lib/webhook/model/messageEditedEvent.ts +32 -0
  98. package/node_modules/@line/bot-sdk/lib/webhook/model/models.ts +1 -0
  99. package/node_modules/@line/bot-sdk/lib/webhook/model/stickerMessageContent.ts +1 -1
  100. package/node_modules/@line/bot-sdk/package.json +3 -3
  101. package/node_modules/@types/node/README.md +1 -1
  102. package/node_modules/@types/node/fs/promises.d.ts +16 -5
  103. package/node_modules/@types/node/package.json +3 -3
  104. package/npm-shrinkwrap.json +10 -10
  105. package/openclaw.plugin.json +0 -18
  106. package/package.json +71 -6
  107. package/dist/channel.runtime-DFgZ_Xwn.js +0 -4
  108. package/dist/monitor.runtime-CZuQgsEE.js +0 -2
  109. package/dist/outbound.runtime-DnpU0Cms.js +0 -2
@@ -1,22 +1,23 @@
1
- import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-DJVOv1JI.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-XceqeKRP.js";
1
+ import { i as resolveLineAccount, r as resolveDefaultLineAccountId } from "./accounts-BUBOoW5Q.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 { createMessageReceiveContext } 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";
@@ -25,7 +26,6 @@ import { getRuntimeConfig } from "openclaw/plugin-sdk/runtime-config-snapshot";
25
26
  import { resolveStableChannelMessageIngress } from "openclaw/plugin-sdk/channel-ingress-runtime";
26
27
  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
- import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
29
29
  import { resolveAgentRoute, resolveInboundLastRouteSessionKey } from "openclaw/plugin-sdk/routing";
30
30
  import { resolveAllowlistProviderRuntimeGroupPolicy, resolveDefaultGroupPolicy, warnMissingProviderGroupPolicyFallbackOnce } from "openclaw/plugin-sdk/runtime-group-policy";
31
31
  import { finalizeInboundContext } from "openclaw/plugin-sdk/reply-dispatch-runtime";
@@ -53,44 +53,85 @@ const CONTENT_READY_BASE_DELAY_MS = 500;
53
53
  const CONTENT_READY_MAX_DELAY_MS = 4e3;
54
54
  const CONTENT_READY_TIMEOUT_MS = 15e3;
55
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
+ };
56
62
  function contentBackoffDelayMs(attempt) {
57
63
  return Math.min(CONTENT_READY_BASE_DELAY_MS * 2 ** attempt, CONTENT_READY_MAX_DELAY_MS);
58
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
+ }
59
94
  async function fetchLineContentWhenReady(messageId, channelAccessToken) {
60
95
  const controller = new AbortController();
61
96
  const deadline = setTimeout(() => controller.abort(), CONTENT_READY_TIMEOUT_MS);
62
97
  deadline.unref();
63
98
  try {
64
99
  for (let attempt = 0; attempt < CONTENT_READY_MAX_ATTEMPTS; attempt++) {
65
- const response = await fetchWithRuntimeDispatcherOrMockedGlobal(`${LINE_CONTENT_BASE_URL}/${encodeURIComponent(messageId)}/content`, {
100
+ const response = await fetchWithRuntimeDispatcherOrMockedGlobal(lineContentUrl(messageId), {
66
101
  headers: { Authorization: `Bearer ${channelAccessToken}` },
67
102
  redirect: "error",
68
103
  signal: controller.signal
69
104
  });
70
105
  if (response.status === 200) {
71
- if (!response.body) throw new Error(`LINE media response for message ${messageId} had no body`);
72
- return Readable.fromWeb(response.body);
106
+ if (!response.body) throw new RetryableLineMediaFetchError(`LINE media response for message ${messageId} had no body`);
107
+ return response;
73
108
  }
74
- await response.body?.cancel();
75
- 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 });
76
111
  if (attempt < CONTENT_READY_MAX_ATTEMPTS - 1) await setTimeout$1(contentBackoffDelayMs(attempt), void 0, { signal: controller.signal });
77
112
  }
78
113
  } catch (err) {
79
- 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 });
80
- 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 });
81
117
  } finally {
82
118
  clearTimeout(deadline);
83
119
  }
84
- 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;
85
127
  }
86
128
  async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 * 1024 * 1024, options) {
87
- const content = await fetchLineContentWhenReady(messageId, channelAccessToken);
129
+ const response = await fetchLineContentWhenReady(messageId, channelAccessToken);
88
130
  let saved;
89
131
  try {
90
- 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);
91
133
  } catch (err) {
92
- content.destroy();
93
- await finished(content).catch(() => void 0);
134
+ await response.body?.cancel().catch(() => void 0);
94
135
  throw err;
95
136
  }
96
137
  logVerbose(`line: persisted media ${messageId} to ${saved.path} (${saved.size} bytes)`);
@@ -103,7 +144,11 @@ async function downloadLineMedia(messageId, channelAccessToken, maxBytes = 10 *
103
144
  //#endregion
104
145
  //#region extensions/line/src/auto-reply-delivery.ts
105
146
  function toLineDeliveryError(error) {
106
- 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;
107
152
  }
108
153
  function markLineVisibleDeliveryError(error) {
109
154
  const deliveryError = toLineDeliveryError(error);
@@ -114,7 +159,7 @@ function markLineVisibleDeliveryError(error) {
114
159
  });
115
160
  return deliveryError;
116
161
  }
117
- const visibleError = new Error("LINE rich or media message send failed", { cause: deliveryError });
162
+ const visibleError = new Error("LINE message send failed", { cause: deliveryError });
118
163
  Object.assign(visibleError, {
119
164
  sentBeforeError: true,
120
165
  visibleReplySent: true
@@ -136,14 +181,25 @@ async function deliverLineAutoReply(params) {
136
181
  }
137
182
  };
138
183
  const replyVisible = (...args) => sendVisible(() => deps.replyMessageLine(...args));
139
- const pushTextVisible = (...args) => sendVisible(() => deps.pushMessageLine(...args));
140
- const pushQuickRepliesVisible = (...args) => sendVisible(() => deps.pushTextMessageWithQuickReplies(...args));
141
- const pushLineMessages = async (messages) => {
184
+ const failedPushSegments = /* @__PURE__ */ new WeakMap();
185
+ const pushLineMessages = async (messages, allowFailedBatchTextRecovery, externalTail = []) => {
142
186
  if (messages.length === 0) return;
143
- for (let i = 0; i < messages.length; i += 5) await sendVisible(() => deps.pushMessagesLine(to, messages.slice(i, i + 5), {
144
- cfg: params.cfg,
145
- accountId
146
- }));
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
+ }
147
203
  };
148
204
  const sendLineMessages = async (messages, allowReplyToken) => {
149
205
  if (messages.length === 0) return;
@@ -157,12 +213,13 @@ async function deliverLineAutoReply(params) {
157
213
  });
158
214
  } catch (err) {
159
215
  deps.onReplyError?.(err);
160
- await pushLineMessages(replyBatch);
216
+ await pushLineMessages(replyBatch, getLineHttpError(err)?.status === 400, remaining.slice(replyBatch.length));
217
+ } finally {
218
+ replyTokenUsed = true;
161
219
  }
162
- replyTokenUsed = true;
163
220
  remaining = remaining.slice(replyBatch.length);
164
221
  }
165
- if (remaining.length > 0) await pushLineMessages(remaining);
222
+ if (remaining.length > 0) await pushLineMessages(remaining, true);
166
223
  };
167
224
  const richMessages = [];
168
225
  const hasQuickReplies = Boolean(lineData.quickReplies?.length);
@@ -187,82 +244,59 @@ async function deliverLineAutoReply(params) {
187
244
  trackingId: lineData.trackingId
188
245
  };
189
246
  const mediaMessages = [];
190
- let richMediaError;
247
+ let deliveryError;
191
248
  for (const rawUrl of mediaUrls) {
192
249
  const url = rawUrl?.trim();
193
250
  if (!url) continue;
194
251
  try {
195
252
  mediaMessages.push(await deps.buildMediaMessage(url, mediaOpts, to));
196
253
  } catch (err) {
197
- richMediaError ??= err;
254
+ deliveryError ??= err;
198
255
  }
199
256
  }
200
- if (chunks.length > 0) {
201
- const hasRichOrMedia = richMessages.length > 0 || mediaMessages.length > 0;
202
- const sendRichBeforeText = hasQuickReplies && hasRichOrMedia;
203
- if (sendRichBeforeText) try {
204
- await sendLineMessages([...richMessages, ...mediaMessages], false);
205
- } catch (err) {
206
- richMediaError ??= err;
207
- }
208
- const { replyTokenUsed: nextReplyTokenUsed } = await deps.sendLineReplyChunks({
209
- to,
210
- chunks,
211
- quickReplies: lineData.quickReplies,
212
- replyToken,
213
- replyTokenUsed,
214
- cfg: params.cfg,
215
- accountId,
216
- replyMessageLine: replyVisible,
217
- pushMessageLine: pushTextVisible,
218
- pushTextMessageWithQuickReplies: pushQuickRepliesVisible,
219
- createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies,
220
- onReplyError: deps.onReplyError
221
- });
222
- replyTokenUsed = nextReplyTokenUsed;
223
- if (!sendRichBeforeText) try {
224
- await sendLineMessages(richMessages, false);
225
- if (mediaMessages.length > 0) await sendLineMessages(mediaMessages, false);
226
- } catch (err) {
227
- richMediaError ??= err;
228
- }
229
- } else {
230
- const combined = [...richMessages, ...mediaMessages];
231
- if (hasQuickReplies && combined.length === 0) {
232
- const { replyTokenUsed: nextReplyTokenUsed } = await deps.sendLineReplyChunks({
233
- to,
234
- chunks: [buildLineQuickReplyFallbackText(lineData.quickReplies)],
235
- quickReplies: lineData.quickReplies,
236
- replyToken,
237
- replyTokenUsed,
238
- cfg: params.cfg,
239
- accountId,
240
- replyMessageLine: replyVisible,
241
- pushMessageLine: pushTextVisible,
242
- pushTextMessageWithQuickReplies: pushQuickRepliesVisible,
243
- createTextMessageWithQuickReplies: deps.createTextMessageWithQuickReplies,
244
- onReplyError: deps.onReplyError
245
- });
246
- replyTokenUsed = nextReplyTokenUsed;
247
- } else {
248
- if (hasQuickReplies && combined.length > 0) {
249
- const quickReply = deps.createQuickReplyItems(lineData.quickReplies);
250
- const targetIndex = replyToken && !replyTokenUsed ? Math.min(4, combined.length - 1) : combined.length - 1;
251
- combined[targetIndex] = {
252
- ...combined[targetIndex],
253
- quickReply
254
- };
255
- }
256
- await sendLineMessages(combined, true);
257
- }
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
+ };
273
+ }
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 {}
258
292
  }
259
- if (richMediaError !== void 0) {
260
- if (!visibleReplySent) throw toLineDeliveryError(richMediaError);
293
+ if (deliveryError !== void 0) {
294
+ if (!visibleReplySent) throw toLineDeliveryError(deliveryError);
261
295
  return {
262
296
  status: "partial",
263
297
  replyTokenUsed,
264
298
  visibleReplySent: true,
265
- error: markLineVisibleDeliveryError(richMediaError)
299
+ error: markLineVisibleDeliveryError(deliveryError)
266
300
  };
267
301
  }
268
302
  return {
@@ -398,13 +432,13 @@ function extractMessageText(message) {
398
432
  }
399
433
  return "";
400
434
  }
401
- function extractMediaPlaceholder(message) {
435
+ function extractNativeMediaKind(message) {
402
436
  switch (message.type) {
403
- case "image": return "<media:image>";
404
- case "video": return "<media:video>";
405
- case "audio": return "<media:audio>";
406
- case "file": return "<media:document>";
407
- 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;
408
442
  }
409
443
  }
410
444
  function resolveLineConversationLabel(params) {
@@ -440,6 +474,7 @@ async function finalizeLineInboundContext(params) {
440
474
  sessionKey: params.route.sessionKey
441
475
  });
442
476
  const agentBody = params.agentBody ?? params.rawBody;
477
+ const media = toInboundMediaFacts(params.media);
443
478
  const body = formatInboundEnvelope({
444
479
  channel: "LINE",
445
480
  from: conversationLabel,
@@ -458,6 +493,7 @@ async function finalizeLineInboundContext(params) {
458
493
  From: fromAddress,
459
494
  To: toAddress,
460
495
  SessionKey: params.route.sessionKey,
496
+ DmScope: params.route.dmScope,
461
497
  AccountId: params.route.accountId,
462
498
  ChatType: params.source.isGroup ? "group" : "direct",
463
499
  ConversationLabel: 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 {
@@ -699,37 +722,9 @@ const LINE_DOWNLOADABLE_MESSAGE_TYPES = /* @__PURE__ */ new Set([
699
722
  function isDownloadableLineMessageType(messageType) {
700
723
  return LINE_DOWNLOADABLE_MESSAGE_TYPES.has(messageType);
701
724
  }
702
- const LINE_WEBHOOK_REPLAY_WINDOW_MS = 600 * 1e3;
703
- const LINE_WEBHOOK_REPLAY_MAX_ENTRIES = 4096;
704
725
  function normalizeLineIngressEntry(value) {
705
726
  return normalizeLineAllowEntry(value) || null;
706
727
  }
707
- function createLineWebhookReplayCache() {
708
- return createChannelReplayGuard({
709
- dedupe: {
710
- ttlMs: LINE_WEBHOOK_REPLAY_WINDOW_MS,
711
- memoryMaxSize: LINE_WEBHOOK_REPLAY_MAX_ENTRIES
712
- },
713
- buildReplayKey: ({ event, accountId }) => buildLineWebhookReplayKey(event, accountId)?.key
714
- });
715
- }
716
- function buildLineWebhookReplayKey(event, accountId) {
717
- if (event.type === "message") {
718
- const messageId = event.message?.id?.trim();
719
- if (messageId) return {
720
- key: `${accountId}|message:${messageId}`,
721
- eventId: `message:${messageId}`
722
- };
723
- }
724
- const eventId = event.webhookEventId?.trim();
725
- if (!eventId) return null;
726
- const source = event.source;
727
- const sourceId = source?.type === "group" ? `group:${source.groupId ?? ""}` : source?.type === "room" ? `room:${source.roomId ?? ""}` : `user:${source?.userId ?? ""}`;
728
- return {
729
- key: `${accountId}|${event.type}|${sourceId}|${eventId}`,
730
- eventId: `event:${eventId}`
731
- };
732
- }
733
728
  function resolveLineGroupConfig(params) {
734
729
  return resolveLineGroupConfigEntry(params.config.groups, {
735
730
  groupId: params.groupId,
@@ -980,6 +975,7 @@ async function handleMessageEvent(event, context) {
980
975
  contentType: media.contentType
981
976
  });
982
977
  } catch (err) {
978
+ if (isRetryableLineInboundMediaError(err)) throw err;
983
979
  mediaUnavailable = true;
984
980
  const errMsg = String(err);
985
981
  if (errMsg.includes("exceeds") && errMsg.includes("limit")) logVerbose(`line: media exceeds size limit for message ${message.id}`);
@@ -998,7 +994,7 @@ async function handleMessageEvent(event, context) {
998
994
  logVerbose("line: skipping empty message");
999
995
  return;
1000
996
  }
1001
- await processMessage(messageContext);
997
+ await processMessage(messageContext, context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {});
1002
998
  historyReservation.commit();
1003
999
  } finally {
1004
1000
  historyReservation.release();
@@ -1032,30 +1028,12 @@ async function handlePostbackEvent(event, context) {
1032
1028
  commandAuthorized: decision.commandAccess.authorized
1033
1029
  });
1034
1030
  if (!postbackContext) return;
1035
- await context.processMessage(postbackContext);
1031
+ await context.processMessage(postbackContext, context.turnAdoptionLifecycle ? { turnAdoptionLifecycle: context.turnAdoptionLifecycle } : {});
1036
1032
  }
1037
1033
  async function handleLineWebhookEvents(events, context) {
1038
1034
  let firstError;
1039
1035
  for (const event of events) try {
1040
- if (!context.replayCache) {
1041
- await handleLineWebhookEvent(event, context);
1042
- continue;
1043
- }
1044
- const replayEvent = {
1045
- event,
1046
- accountId: context.account.accountId
1047
- };
1048
- const result = await context.replayCache.processGuarded(replayEvent, async () => await handleLineWebhookEvent(event, context), { onError: "commit" });
1049
- const replayId = buildLineWebhookReplayKey(event, context.account.accountId)?.eventId;
1050
- if (result.kind === "inflight") {
1051
- logVerbose(`line: skipped in-flight replayed webhook event ${replayId ?? "unknown"}`);
1052
- try {
1053
- await result.pending;
1054
- } catch (err) {
1055
- context.runtime.error?.(danger(`line: replayed in-flight event failed: ${String(err)}`));
1056
- firstError ??= err;
1057
- }
1058
- } else if (result.kind === "duplicate") logVerbose(`line: skipped replayed webhook event ${replayId ?? "unknown"}`);
1036
+ await handleLineWebhookEvent(event, context);
1059
1037
  } catch (err) {
1060
1038
  context.runtime.error?.(danger(`line: event handler failed: ${String(err)}`));
1061
1039
  firstError ??= err;
@@ -1093,6 +1071,257 @@ function toLintErrorObject(value, fallbackMessage) {
1093
1071
  return error;
1094
1072
  }
1095
1073
  //#endregion
1074
+ //#region extensions/line/src/webhook-spool.ts
1075
+ const LINE_WEBHOOK_SPOOL_VERSION = 1;
1076
+ const LINE_WEBHOOK_DRAIN_INTERVAL_MS = 500;
1077
+ const LINE_WEBHOOK_MAX_CONCURRENT_DELIVERIES = 8;
1078
+ const LINE_WEBHOOK_DRAIN_SCAN_LIMIT = 100;
1079
+ const LINE_WEBHOOK_ACTIVE_DELIVERY_STOP_GRACE_MS = 5e3;
1080
+ const LINE_WEBHOOK_TOMBSTONE_TTL_MS = 720 * 60 * 6e4;
1081
+ const LINE_WEBHOOK_TOMBSTONE_MAX_ENTRIES = 4096;
1082
+ var LineWebhookPayloadError = class extends Error {
1083
+ constructor(message, options) {
1084
+ super(message, options);
1085
+ this.name = "LineWebhookPayloadError";
1086
+ }
1087
+ };
1088
+ var LineWebhookTerminalDeliveryError = class extends Error {
1089
+ constructor(message, options) {
1090
+ super(message, options);
1091
+ this.reason = "delivery-side-effects-committed";
1092
+ this.name = "LineWebhookTerminalDeliveryError";
1093
+ }
1094
+ };
1095
+ function nonEmptyString(value) {
1096
+ return typeof value === "string" && value.trim() ? value.trim() : null;
1097
+ }
1098
+ /** Message ids preserve the shipped replay-guard keyspace; other events use LINE's delivery id. */
1099
+ function eventIdFor(event) {
1100
+ if (!event || typeof event !== "object") throw new LineWebhookPayloadError("LINE webhook event must be an object.");
1101
+ const candidate = event;
1102
+ if (candidate.type === "message") {
1103
+ const messageId = nonEmptyString(candidate.message?.id);
1104
+ if (messageId) return `message:${messageId}`;
1105
+ }
1106
+ const webhookEventId = nonEmptyString(candidate.webhookEventId);
1107
+ if (webhookEventId) return `event:${webhookEventId}`;
1108
+ throw new LineWebhookPayloadError("LINE webhook event is missing a stable delivery id.");
1109
+ }
1110
+ function laneKeyFor(event, eventId) {
1111
+ if (!event || typeof event !== "object") return eventId;
1112
+ const source = event.source;
1113
+ if (source?.type === "group") {
1114
+ const groupId = nonEmptyString(source.groupId);
1115
+ if (groupId) return `group:${groupId}`;
1116
+ }
1117
+ if (source?.type === "room") {
1118
+ const roomId = nonEmptyString(source.roomId);
1119
+ if (roomId) return `room:${roomId}`;
1120
+ }
1121
+ if (source?.type === "user") {
1122
+ const userId = nonEmptyString(source.userId);
1123
+ if (userId) return `user:${userId}`;
1124
+ }
1125
+ return eventId;
1126
+ }
1127
+ function parseStoredEvent(rawEvent) {
1128
+ let event;
1129
+ try {
1130
+ event = JSON.parse(rawEvent);
1131
+ } catch (error) {
1132
+ throw new LineWebhookPayloadError("LINE webhook event JSON is invalid.", { cause: error });
1133
+ }
1134
+ return event;
1135
+ }
1136
+ function errorText(error) {
1137
+ return error instanceof Error ? error.message : String(error);
1138
+ }
1139
+ function isLineAuthenticationFailure(error) {
1140
+ if (!error || typeof error !== "object") return false;
1141
+ const status = error.status;
1142
+ return status === 401 || status === 403;
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
+ }
1155
+ function createLineWebhookSpool(options) {
1156
+ const queue = options.queue ?? getLineRuntime().state.openChannelIngressQueue({ accountId: options.accountId });
1157
+ const activeDeliveries = /* @__PURE__ */ new Set();
1158
+ const deferredClaims = /* @__PURE__ */ new Map();
1159
+ let acceptsDeferredClaims = true;
1160
+ const monitor = createChannelIngressMonitor({
1161
+ queue,
1162
+ inspect: ({ event }) => {
1163
+ const eventId = eventIdFor(event);
1164
+ return {
1165
+ eventId,
1166
+ laneKey: laneKeyFor(event, eventId)
1167
+ };
1168
+ },
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) => {
1197
+ const boundLifecycle = bindIngressLifecycleToReplyOptions(lifecycle).turnAdoptionLifecycle;
1198
+ let handedOff = false;
1199
+ let resolveDeferredClaim;
1200
+ const deferredClaim = new Promise((resolve) => {
1201
+ resolveDeferredClaim = resolve;
1202
+ });
1203
+ let deferredClaimSettled = false;
1204
+ const settleDeferredClaim = () => {
1205
+ if (deferredClaimSettled) return;
1206
+ deferredClaimSettled = true;
1207
+ if (deferredClaims.get(claim.id) === deferredClaim) deferredClaims.delete(claim.id);
1208
+ resolveDeferredClaim();
1209
+ };
1210
+ const delivery = options.deliver(event, destination, { turnAdoptionLifecycle: {
1211
+ ...boundLifecycle,
1212
+ onAdopted: async () => {
1213
+ handedOff = true;
1214
+ try {
1215
+ await boundLifecycle.onAdopted();
1216
+ } finally {
1217
+ settleDeferredClaim();
1218
+ }
1219
+ },
1220
+ onDeferred: () => {
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);
1230
+ boundLifecycle.onDeferred();
1231
+ },
1232
+ onAbandoned: async () => {
1233
+ handedOff = true;
1234
+ try {
1235
+ await boundLifecycle.onAbandoned();
1236
+ } finally {
1237
+ settleDeferredClaim();
1238
+ }
1239
+ }
1240
+ } });
1241
+ activeDeliveries.add(delivery);
1242
+ try {
1243
+ await delivery;
1244
+ } finally {
1245
+ activeDeliveries.delete(delivery);
1246
+ }
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)}`))
1293
+ });
1294
+ let stopTask;
1295
+ return {
1296
+ accept: async (body) => {
1297
+ const events = body.events ?? [];
1298
+ if (events.length === 0) return;
1299
+ await monitor.admitBatch(events.map((event) => ({
1300
+ event,
1301
+ destination: body.destination ?? ""
1302
+ })), { receivedAt: Date.now() });
1303
+ },
1304
+ start: () => {
1305
+ if (!stopTask) monitor.start();
1306
+ },
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;
1321
+ }
1322
+ };
1323
+ }
1324
+ //#endregion
1096
1325
  //#region extensions/line/src/bot.ts
1097
1326
  function createLineBot(opts) {
1098
1327
  const runtime = opts.runtime ?? createNonExitingRuntime();
@@ -1105,24 +1334,26 @@ function createLineBot(opts) {
1105
1334
  const processMessage = opts.onMessage ?? (async () => {
1106
1335
  logVerbose("line: no message handler configured");
1107
1336
  });
1108
- const replayCache = createLineWebhookReplayCache();
1109
1337
  const groupHistories = /* @__PURE__ */ new Map();
1110
- const handleWebhook = async (body) => {
1111
- if (!body.events || body.events.length === 0) return;
1112
- await handleLineWebhookEvents(body.events, {
1338
+ const spool = createLineWebhookSpool({
1339
+ accountId: account.accountId,
1340
+ runtime,
1341
+ deliver: async (event, _destination, control) => await handleLineWebhookEvents([event], {
1113
1342
  cfg,
1114
1343
  account,
1115
1344
  runtime,
1116
1345
  mediaMaxBytes,
1117
1346
  processMessage,
1118
- replayCache,
1347
+ ...control.turnAdoptionLifecycle ? { turnAdoptionLifecycle: control.turnAdoptionLifecycle } : {},
1119
1348
  groupHistories,
1120
1349
  historyLimit: cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT
1121
- });
1122
- };
1350
+ })
1351
+ });
1352
+ spool.start();
1123
1353
  return {
1124
- handleWebhook,
1125
- account
1354
+ handleWebhook: spool.accept,
1355
+ account,
1356
+ stop: spool.stop
1126
1357
  };
1127
1358
  }
1128
1359
  //#endregion
@@ -1140,56 +1371,6 @@ function resolveLineDurableReplyOptions(params) {
1140
1371
  return { to: params.to };
1141
1372
  }
1142
1373
  //#endregion
1143
- //#region extensions/line/src/reply-chunks.ts
1144
- async function sendLineReplyChunks(params) {
1145
- const quickReplies = params.quickReplies?.length ? params.quickReplies : void 0;
1146
- let replyTokenUsed = Boolean(params.replyTokenUsed);
1147
- if (params.chunks.length === 0) return { replyTokenUsed };
1148
- if (params.replyToken && !replyTokenUsed) {
1149
- let replySucceeded = false;
1150
- try {
1151
- const replyBatch = params.chunks.slice(0, 5);
1152
- const remaining = params.chunks.slice(replyBatch.length);
1153
- const replyMessages = replyBatch.map((chunk) => ({
1154
- type: "text",
1155
- text: chunk
1156
- }));
1157
- if (quickReplies && remaining.length === 0 && replyMessages.length > 0) {
1158
- const lastIndex = replyMessages.length - 1;
1159
- replyMessages[lastIndex] = params.createTextMessageWithQuickReplies(expectDefined(replyBatch[lastIndex], "last non-empty LINE reply batch chunk"), quickReplies);
1160
- }
1161
- await params.replyMessageLine(params.replyToken, replyMessages, {
1162
- cfg: params.cfg,
1163
- accountId: params.accountId
1164
- });
1165
- replyTokenUsed = true;
1166
- replySucceeded = true;
1167
- for (const [i, chunk] of remaining.entries()) if (i === remaining.length - 1 && quickReplies) await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, {
1168
- cfg: params.cfg,
1169
- accountId: params.accountId
1170
- });
1171
- else await params.pushMessageLine(params.to, chunk, {
1172
- cfg: params.cfg,
1173
- accountId: params.accountId
1174
- });
1175
- return { replyTokenUsed };
1176
- } catch (err) {
1177
- if (replySucceeded) throw err;
1178
- params.onReplyError?.(err);
1179
- replyTokenUsed = true;
1180
- }
1181
- }
1182
- for (const [i, chunk] of params.chunks.entries()) if (i === params.chunks.length - 1 && quickReplies) await params.pushTextMessageWithQuickReplies(params.to, chunk, quickReplies, {
1183
- cfg: params.cfg,
1184
- accountId: params.accountId
1185
- });
1186
- else await params.pushMessageLine(params.to, chunk, {
1187
- cfg: params.cfg,
1188
- accountId: params.accountId
1189
- });
1190
- return { replyTokenUsed };
1191
- }
1192
- //#endregion
1193
1374
  //#region extensions/line/src/signature.ts
1194
1375
  function validateLineSignature(body, signature, channelSecret) {
1195
1376
  return safeEqualSecret(signature, crypto.createHmac("SHA256", channelSecret).update(body).digest("base64"));
@@ -1214,9 +1395,6 @@ async function readLineWebhookRequestBody(req, maxBytes = LINE_WEBHOOK_MAX_BODY_
1214
1395
  timeoutMs
1215
1396
  });
1216
1397
  }
1217
- function logLineWebhookDispatchError(runtime, err) {
1218
- runtime?.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
1219
- }
1220
1398
  function createLineNodeWebhookHandler(params) {
1221
1399
  const maxBodyBytes = params.maxBodyBytes ?? LINE_WEBHOOK_MAX_BODY_BYTES;
1222
1400
  const readBody = params.readBody ?? readLineWebhookRequestBody;
@@ -1239,7 +1417,6 @@ function createLineNodeWebhookHandler(params) {
1239
1417
  res.end(JSON.stringify({ error: "Method Not Allowed" }));
1240
1418
  return;
1241
1419
  }
1242
- let receiveContext;
1243
1420
  try {
1244
1421
  const signatureHeader = req.headers["x-line-signature"];
1245
1422
  const signature = typeof signatureHeader === "string" ? signatureHeader.trim() : Array.isArray(signatureHeader) ? (signatureHeader[0] ?? "").trim() : "";
@@ -1266,24 +1443,14 @@ function createLineNodeWebhookHandler(params) {
1266
1443
  return;
1267
1444
  }
1268
1445
  params.onRequestAuthenticated?.();
1269
- receiveContext = createMessageReceiveContext({
1270
- id: `${Date.now()}:line:webhook`,
1271
- channel: "line",
1272
- message: body,
1273
- ackPolicy: "after_receive_record",
1274
- onAck: () => {
1275
- res.statusCode = 200;
1276
- res.setHeader("Content-Type", "application/json");
1277
- res.end(JSON.stringify({ status: "ok" }));
1278
- }
1279
- });
1280
- if (receiveContext.shouldAckAfter("receive_record")) await receiveContext.ack();
1281
1446
  if (body.events && body.events.length > 0) {
1282
1447
  logVerbose(`line: received ${body.events.length} webhook events`);
1283
- runDetachedWebhookWork(() => params.bot.handleWebhook(body)).catch((err) => logLineWebhookDispatchError(params.runtime, err));
1448
+ await params.bot.handleWebhook(body);
1284
1449
  }
1450
+ res.statusCode = 200;
1451
+ res.setHeader("Content-Type", "application/json");
1452
+ res.end(JSON.stringify({ status: "ok" }));
1285
1453
  } catch (err) {
1286
- await receiveContext?.nack(err);
1287
1454
  if (isRequestBodyLimitError$1(err, "PAYLOAD_TOO_LARGE")) {
1288
1455
  res.statusCode = 413;
1289
1456
  res.setHeader("Content-Type", "application/json");
@@ -1344,7 +1511,7 @@ async function monitorLineProvider(opts) {
1344
1511
  accountId,
1345
1512
  runtime,
1346
1513
  config,
1347
- onMessage: async (ctx) => {
1514
+ onMessage: async (ctx, deliveryControl) => {
1348
1515
  if (!ctx) return;
1349
1516
  const { ctxPayload, replyToken, route } = ctx;
1350
1517
  const shouldShowLoading = Boolean(ctx.userId && !ctx.isGroup);
@@ -1358,14 +1525,23 @@ async function monitorLineProvider(opts) {
1358
1525
  accountId: ctx.accountId
1359
1526
  }) : null;
1360
1527
  logVerbose(`line: received message from ${await displayNamePromise} (${ctxPayload.From})`);
1528
+ let replyTokenUsed = false;
1529
+ let turnAdopted = false;
1530
+ const ingressLifecycle = deliveryControl.turnAdoptionLifecycle;
1361
1531
  try {
1362
1532
  const textLimit = 5e3;
1363
- let replyTokenUsed = false;
1364
- const core = getLineRuntime();
1365
- const turnResult = await core.channel.inbound.run({
1533
+ const turnResult = await getLineRuntime().channel.inbound.run({
1366
1534
  channel: "line",
1367
1535
  accountId: route.accountId,
1368
1536
  raw: ctx,
1537
+ turnAdoptionLifecycle: {
1538
+ ...ingressLifecycle,
1539
+ admission: "exclusive",
1540
+ onAdopted: async () => {
1541
+ await ingressLifecycle?.onAdopted();
1542
+ turnAdopted = true;
1543
+ }
1544
+ },
1369
1545
  adapter: {
1370
1546
  ingest: () => ({
1371
1547
  id: ctxPayload.MessageSid ?? `${ctxPayload.From}:${Date.now()}`,
@@ -1375,14 +1551,14 @@ async function monitorLineProvider(opts) {
1375
1551
  cfg: config,
1376
1552
  channel: "line",
1377
1553
  accountId: route.accountId,
1378
- agentId: route.agentId,
1379
- routeSessionKey: route.sessionKey,
1380
- storePath: ctx.turn.storePath,
1554
+ route: {
1555
+ agentId: route.agentId,
1556
+ sessionKey: route.sessionKey
1557
+ },
1381
1558
  ctxPayload,
1382
- recordInboundSession: core.channel.session.recordInboundSession,
1383
- dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
1384
1559
  record: ctx.turn.record,
1385
1560
  replyPipeline: {},
1561
+ ...ingressLifecycle?.abortSignal ? { replyOptions: { abortSignal: ingressLifecycle.abortSignal } } : {},
1386
1562
  delivery: {
1387
1563
  durable: (payload, info) => resolveLineDurableReplyOptions({
1388
1564
  payload,
@@ -1410,12 +1586,8 @@ async function monitorLineProvider(opts) {
1410
1586
  buildTemplateMessageFromPayload,
1411
1587
  processLineMessage,
1412
1588
  chunkMarkdownText,
1413
- sendLineReplyChunks,
1414
1589
  replyMessageLine,
1415
- pushMessageLine,
1416
- pushTextMessageWithQuickReplies,
1417
1590
  createQuickReplyItems,
1418
- createTextMessageWithQuickReplies,
1419
1591
  pushMessagesLine,
1420
1592
  createFlexMessage,
1421
1593
  buildMediaMessage: buildLineMediaMessage,
@@ -1439,17 +1611,8 @@ async function monitorLineProvider(opts) {
1439
1611
  if (!hasFinalInboundReplyDispatch(turnResult.dispatched ? turnResult.dispatchResult : void 0)) logVerbose(`line: no response generated for message from ${ctxPayload.From}`);
1440
1612
  } catch (err) {
1441
1613
  runtime.error?.(danger(`line: auto-reply failed: ${String(err)}`));
1442
- if (replyToken) try {
1443
- await replyMessageLine(replyToken, [{
1444
- type: "text",
1445
- text: "Sorry, I encountered an error processing your message."
1446
- }], {
1447
- cfg: config,
1448
- accountId: ctx.accountId
1449
- });
1450
- } catch (replyErr) {
1451
- runtime.error?.(danger(`line: error reply failed: ${String(replyErr)}`));
1452
- }
1614
+ if (turnAdopted || replyTokenUsed) throw new LineWebhookTerminalDeliveryError("LINE delivery failed after consuming the event reply token.", { cause: err });
1615
+ throw err;
1453
1616
  } finally {
1454
1617
  stopLoading?.();
1455
1618
  }
@@ -1527,16 +1690,13 @@ async function monitorLineProvider(opts) {
1527
1690
  res.end(JSON.stringify({ error: "Invalid webhook payload" }));
1528
1691
  return;
1529
1692
  }
1530
- requestLifecycle.release();
1531
- res.statusCode = 200;
1532
- res.setHeader("Content-Type", "application/json");
1533
- res.end(JSON.stringify({ status: "ok" }));
1534
1693
  if (body.events && body.events.length > 0) {
1535
1694
  logVerbose(`line: received ${body.events.length} webhook events`);
1536
- runDetachedWebhookWork(() => match.target.bot.handleWebhook(body)).catch((err) => {
1537
- match.target.runtime.error?.(danger(`line webhook dispatch failed: ${String(err)}`));
1538
- });
1695
+ await match.target.bot.handleWebhook(body);
1539
1696
  }
1697
+ res.statusCode = 200;
1698
+ res.setHeader("Content-Type", "application/json");
1699
+ res.end(JSON.stringify({ status: "ok" }));
1540
1700
  } catch (err) {
1541
1701
  if (isRequestBodyLimitError(err, "PAYLOAD_TOO_LARGE")) {
1542
1702
  res.statusCode = 413;
@@ -1564,23 +1724,29 @@ async function monitorLineProvider(opts) {
1564
1724
  });
1565
1725
  logVerbose(`line: registered webhook handler at ${normalizedPath}`);
1566
1726
  let stopped = false;
1727
+ let stopPromise;
1567
1728
  const stopHandler = () => {
1568
- if (stopped) return;
1729
+ if (stopPromise) return stopPromise;
1730
+ if (stopped) return Promise.resolve();
1569
1731
  stopped = true;
1570
1732
  logVerbose(`line: stopping provider for account ${resolvedAccountId}`);
1571
1733
  unregisterHttp();
1734
+ stopPromise = bot.stop();
1735
+ return stopPromise;
1572
1736
  };
1573
- if (abortSignal?.aborted) stopHandler();
1737
+ const stopOnAbort = () => void stopHandler();
1738
+ if (abortSignal?.aborted) await stopHandler();
1574
1739
  else if (abortSignal) {
1575
- abortSignal.addEventListener("abort", stopHandler, { once: true });
1740
+ abortSignal.addEventListener("abort", stopOnAbort, { once: true });
1576
1741
  await waitForAbortSignal(abortSignal);
1742
+ await stopHandler();
1577
1743
  }
1578
1744
  return {
1579
1745
  account: bot.account,
1580
1746
  handleWebhook: bot.handleWebhook,
1581
- stop: () => {
1582
- stopHandler();
1583
- abortSignal?.removeEventListener("abort", stopHandler);
1747
+ stop: async () => {
1748
+ await stopHandler();
1749
+ abortSignal?.removeEventListener("abort", stopOnAbort);
1584
1750
  }
1585
1751
  };
1586
1752
  }