@heylemon/lemonade 2026.2.25 → 2026.2.27

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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2026.2.25",
3
- "commit": "c541a48e05b68c3a4d189a35f48649c388f57fa1",
4
- "builtAt": "2026-02-19T17:46:07.135Z"
2
+ "version": "2026.2.27",
3
+ "commit": "2909f76416b6b24d6d692f9273ba5f452283248d",
4
+ "builtAt": "2026-02-20T04:00:13.136Z"
5
5
  }
@@ -1 +1 @@
1
- 140b3b7c47510604aa1c59436ecf4b03ac92f2762d85a0c50581d82725391c66
1
+ 97f5c46c50948d0e879a973bf28450f752b5c3beda881fe18db3cdebcb14f395
@@ -110,6 +110,7 @@ export function createChannelManager(opts) {
110
110
  log.error?.(`[${id}] channel exited: ${message}`);
111
111
  })
112
112
  .finally(() => {
113
+ const wasAborted = abort.signal.aborted;
113
114
  store.aborts.delete(id);
114
115
  store.tasks.delete(id);
115
116
  setRuntime(channelId, id, {
@@ -117,6 +118,23 @@ export function createChannelManager(opts) {
117
118
  running: false,
118
119
  lastStopAt: Date.now(),
119
120
  });
121
+ // Auto-restart channels that exit unexpectedly (not intentionally
122
+ // stopped via abort). This is a safety net: the inner reconnect
123
+ // loop inside monitorWebChannel handles transient failures, but if
124
+ // the channel exits entirely (e.g., max reconnect attempts
125
+ // exhausted, or an unexpected throw), we restart it at this level.
126
+ if (!wasAborted) {
127
+ const RESTART_DELAY_MS = 30_000;
128
+ log.info?.(`[${id}] scheduling auto-restart in ${RESTART_DELAY_MS / 1000}s`);
129
+ setTimeout(() => {
130
+ if (store.tasks.has(id))
131
+ return; // already restarted
132
+ log.info?.(`[${id}] auto-restarting channel`);
133
+ void startChannel(channelId, id).catch((restartErr) => {
134
+ log.error?.(`[${id}] auto-restart failed: ${formatErrorMessage(restartErr)}`);
135
+ });
136
+ }, RESTART_DELAY_MS);
137
+ }
120
138
  });
121
139
  store.tasks.set(id, tracked);
122
140
  }));
@@ -16,7 +16,7 @@ import { resolveWhatsAppAccount } from "../accounts.js";
16
16
  import { setActiveWebListener } from "../active-listener.js";
17
17
  import { monitorWebInbox } from "../inbound.js";
18
18
  import { computeBackoff, newConnectionId, resolveHeartbeatSeconds, resolveReconnectPolicy, sleepWithAbort, } from "../reconnect.js";
19
- import { formatError, getWebAuthAgeMs, readWebSelfId } from "../session.js";
19
+ import { formatError, getStatusCode, getWebAuthAgeMs, readWebSelfId } from "../session.js";
20
20
  import { DEFAULT_WEB_MEDIA_BYTES } from "./constants.js";
21
21
  import { whatsappHeartbeatLog, whatsappLog } from "./loggers.js";
22
22
  import { buildMentionConfig } from "./mentions.js";
@@ -142,24 +142,70 @@ export async function monitorWebChannel(verbose, listenerFactory = monitorWebInb
142
142
  return false;
143
143
  return !hasControlCommand(msg.body, cfg);
144
144
  };
145
- const listener = await (listenerFactory ?? monitorWebInbox)({
146
- verbose,
147
- accountId: account.accountId,
148
- authDir: account.authDir,
149
- mediaMaxMb: account.mediaMaxMb,
150
- sendReadReceipts: account.sendReadReceipts,
151
- debounceMs: inboundDebounceMs,
152
- shouldDebounce,
153
- onMessage: async (msg) => {
154
- handledMessages += 1;
155
- lastMessageAt = Date.now();
156
- status.lastMessageAt = lastMessageAt;
157
- status.lastEventAt = lastMessageAt;
158
- emitStatus();
159
- _lastInboundMsg = msg;
160
- await onMessage(msg);
161
- },
162
- });
145
+ let listener;
146
+ try {
147
+ listener = await (listenerFactory ?? monitorWebInbox)({
148
+ verbose,
149
+ accountId: account.accountId,
150
+ authDir: account.authDir,
151
+ mediaMaxMb: account.mediaMaxMb,
152
+ sendReadReceipts: account.sendReadReceipts,
153
+ debounceMs: inboundDebounceMs,
154
+ shouldDebounce,
155
+ onMessage: async (msg) => {
156
+ handledMessages += 1;
157
+ lastMessageAt = Date.now();
158
+ status.lastMessageAt = lastMessageAt;
159
+ status.lastEventAt = lastMessageAt;
160
+ emitStatus();
161
+ _lastInboundMsg = msg;
162
+ await onMessage(msg);
163
+ },
164
+ });
165
+ }
166
+ catch (err) {
167
+ // Initial socket creation or WebSocket handshake failed (e.g., 408
168
+ // timeout, 428 Connection Terminated). Without this catch the error
169
+ // escapes the while-loop and the reconnect backoff is never reached.
170
+ const errStatusCode = getStatusCode(err);
171
+ const errorStr = formatError(err);
172
+ reconnectLogger.warn({ connectionId, error: errorStr, status: errStatusCode }, "web reconnect: initial connection failed");
173
+ status.connected = false;
174
+ status.lastError = errorStr;
175
+ status.lastDisconnect = {
176
+ at: Date.now(),
177
+ status: typeof errStatusCode === "number" ? errStatusCode : undefined,
178
+ error: errorStr,
179
+ loggedOut: errStatusCode === 401,
180
+ };
181
+ emitStatus();
182
+ if (errStatusCode === 401) {
183
+ runtime.error(`WhatsApp session logged out. Run \`${formatCliCommand("lemonade channels login --channel web")}\` to relink.`);
184
+ break;
185
+ }
186
+ reconnectAttempts += 1;
187
+ status.reconnectAttempts = reconnectAttempts;
188
+ emitStatus();
189
+ if (reconnectPolicy.maxAttempts > 0 && reconnectAttempts >= reconnectPolicy.maxAttempts) {
190
+ reconnectLogger.warn({
191
+ connectionId,
192
+ status: errStatusCode,
193
+ reconnectAttempts,
194
+ maxAttempts: reconnectPolicy.maxAttempts,
195
+ }, "web reconnect: max attempts reached after connection failure");
196
+ runtime.error(`WhatsApp Web reconnect: max attempts reached (${reconnectAttempts}/${reconnectPolicy.maxAttempts}). Stopping web monitoring.`);
197
+ break;
198
+ }
199
+ const delay = computeBackoff(reconnectPolicy, reconnectAttempts);
200
+ runtime.error(`WhatsApp Web connection failed (status ${errStatusCode ?? "unknown"}). Retry ${reconnectAttempts}/${reconnectPolicy.maxAttempts || "∞"} in ${formatDurationMs(delay)}… (${errorStr})`);
201
+ try {
202
+ await sleep(delay, abortSignal);
203
+ }
204
+ catch {
205
+ break;
206
+ }
207
+ continue;
208
+ }
163
209
  status.connected = true;
164
210
  status.lastConnectedAt = Date.now();
165
211
  status.lastEventAt = status.lastConnectedAt;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heylemon/lemonade",
3
- "version": "2026.2.25",
3
+ "version": "2026.2.27",
4
4
  "description": "AI gateway CLI for Lemon - local AI assistant with integrations",
5
5
  "publishConfig": {
6
6
  "access": "restricted"