@wu529778790/open-im 1.9.4-beta.4 → 1.9.4-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,6 +32,8 @@ export interface CentrifugeCallbacks {
32
32
  onDisconnected?: (reason?: string) => void;
33
33
  onError?: (error: Error) => void;
34
34
  onMessage?: (chatId: string, msgId: string, content: string) => void;
35
+ /** Called when reconnection has failed too many times — caller should do a full re-registration */
36
+ onPersistentFailure?: () => void;
35
37
  }
36
38
  export declare class WorkBuddyCentrifugeClient {
37
39
  private config;
@@ -41,6 +43,9 @@ export declare class WorkBuddyCentrifugeClient {
41
43
  private extraSubs;
42
44
  private state;
43
45
  private processedMsgIds;
46
+ private consecutiveErrors;
47
+ private pendingReplies;
48
+ private flushing;
44
49
  private static readonly MAX_MSG_ID_CACHE;
45
50
  constructor(config: CentrifugeClientConfig, callbacks?: CentrifugeCallbacks);
46
51
  get logPrefix(): string;
@@ -83,4 +88,12 @@ export declare class WorkBuddyCentrifugeClient {
83
88
  * Clean up old message IDs from cache
84
89
  */
85
90
  private cleanMsgIdCache;
91
+ /**
92
+ * Enqueue a failed reply for later delivery
93
+ */
94
+ private enqueuePendingReply;
95
+ /**
96
+ * Retry all pending replies after a successful reconnection
97
+ */
98
+ private flushPendingReplies;
86
99
  }
@@ -6,6 +6,12 @@ import { WebSocket } from 'ws';
6
6
  import { randomUUID } from 'node:crypto';
7
7
  import { createLogger } from '../logger.js';
8
8
  const log = createLogger('WorkBuddyCentrifuge');
9
+ /** Max consecutive errors before triggering full re-registration */
10
+ const PERSISTENT_FAILURE_THRESHOLD = 5;
11
+ /** Max queued replies awaiting delivery */
12
+ const MAX_PENDING_REPLIES = 20;
13
+ /** Max age (ms) of a queued reply before it's discarded */
14
+ const PENDING_REPLY_TTL_MS = 5 * 60_000;
9
15
  export class WorkBuddyCentrifugeClient {
10
16
  config;
11
17
  callbacks;
@@ -14,6 +20,9 @@ export class WorkBuddyCentrifugeClient {
14
20
  extraSubs = [];
15
21
  state = 'disconnected';
16
22
  processedMsgIds = new Set();
23
+ consecutiveErrors = 0;
24
+ pendingReplies = [];
25
+ flushing = false;
17
26
  static MAX_MSG_ID_CACHE = 1000;
18
27
  constructor(config, callbacks = {}) {
19
28
  this.config = config;
@@ -39,7 +48,9 @@ export class WorkBuddyCentrifugeClient {
39
48
  this.client.on('connected', (ctx) => {
40
49
  log.info(`${this.logPrefix} Connected (transport=${ctx.transport})`);
41
50
  this.state = 'connected';
51
+ this.consecutiveErrors = 0;
42
52
  this.callbacks.onConnected?.();
53
+ this.flushPendingReplies();
43
54
  });
44
55
  this.client.on('disconnected', (ctx) => {
45
56
  log.info(`${this.logPrefix} Disconnected: code=${ctx.code}, reason=${ctx.reason}`);
@@ -56,7 +67,15 @@ export class WorkBuddyCentrifugeClient {
56
67
  });
57
68
  this.client.on('error', (ctx) => {
58
69
  log.error(`${this.logPrefix} Error: ${ctx.error.message}`);
70
+ this.consecutiveErrors++;
59
71
  this.callbacks.onError?.(new Error(ctx.error.message));
72
+ if (this.consecutiveErrors >= PERSISTENT_FAILURE_THRESHOLD) {
73
+ log.warn(`${this.logPrefix} ${this.consecutiveErrors} consecutive errors — triggering full re-registration`);
74
+ this.consecutiveErrors = 0;
75
+ // Stop the current Centrifuge instance so client.ts can create a fresh one
76
+ this.stop();
77
+ this.callbacks.onPersistentFailure?.();
78
+ }
60
79
  });
61
80
  // Create channel subscription
62
81
  this.sub = this.client.newSubscription(this.config.channel, {
@@ -216,7 +235,7 @@ export class WorkBuddyCentrifugeClient {
216
235
  }
217
236
  }
218
237
  if (!sent) {
219
- log.error(`${this.logPrefix} Failed to send COPILOT_RESPONSE after retries`);
238
+ this.enqueuePendingReply(url, httpPayload, this.config.httpAccessToken);
220
239
  }
221
240
  // Release the heartbeat lock so the periodic registration can resume
222
241
  this.config.releaseChannelLockFn?.();
@@ -325,4 +344,57 @@ export class WorkBuddyCentrifugeClient {
325
344
  });
326
345
  }
327
346
  }
347
+ /**
348
+ * Enqueue a failed reply for later delivery
349
+ */
350
+ enqueuePendingReply(url, payload, accessToken) {
351
+ // Evict expired entries
352
+ const now = Date.now();
353
+ this.pendingReplies = this.pendingReplies.filter((r) => now - r.addedAt < PENDING_REPLY_TTL_MS);
354
+ if (this.pendingReplies.length >= MAX_PENDING_REPLIES) {
355
+ const evicted = this.pendingReplies.shift();
356
+ log.warn(`${this.logPrefix} Pending replies full, evicting oldest (msgId=${evicted?.payload.msgId})`);
357
+ }
358
+ this.pendingReplies.push({ url, payload, accessToken, addedAt: now });
359
+ log.warn(`${this.logPrefix} Queued pending reply (queue=${this.pendingReplies.length}, msgId=${payload.msgId})`);
360
+ }
361
+ /**
362
+ * Retry all pending replies after a successful reconnection
363
+ */
364
+ async flushPendingReplies() {
365
+ if (this.flushing || this.pendingReplies.length === 0)
366
+ return;
367
+ this.flushing = true;
368
+ const now = Date.now();
369
+ // Take only non-expired replies
370
+ const toSend = this.pendingReplies.filter((r) => now - r.addedAt < PENDING_REPLY_TTL_MS);
371
+ this.pendingReplies = [];
372
+ if (toSend.length > 0) {
373
+ log.info(`${this.logPrefix} Flushing ${toSend.length} pending reply(ies)`);
374
+ }
375
+ for (const reply of toSend) {
376
+ try {
377
+ const res = await fetch(reply.url, {
378
+ method: 'POST',
379
+ headers: {
380
+ 'Content-Type': 'application/json',
381
+ Authorization: `Bearer ${reply.accessToken}`,
382
+ },
383
+ body: JSON.stringify(reply.payload),
384
+ signal: AbortSignal.timeout(30_000),
385
+ });
386
+ const body = await res.text().catch(() => '');
387
+ if (res.ok) {
388
+ log.info(`${this.logPrefix} Flushed pending reply ok: msgId=${reply.payload.msgId}`);
389
+ }
390
+ else {
391
+ log.error(`${this.logPrefix} Flushed pending reply failed: ${res.status} ${body.substring(0, 200)}`);
392
+ }
393
+ }
394
+ catch (err) {
395
+ log.error(`${this.logPrefix} Flushed pending reply error: msgId=${reply.payload.msgId}`, err);
396
+ }
397
+ }
398
+ this.flushing = false;
399
+ }
328
400
  }
@@ -198,6 +198,15 @@ async function connect() {
198
198
  log.error(`WorkBuddy Centrifuge error: ${error instanceof Error ? error.message : JSON.stringify(error)}`);
199
199
  updateState('error');
200
200
  },
201
+ onPersistentFailure: () => {
202
+ log.warn('WorkBuddy Centrifuge persistent failure detected — doing full re-registration');
203
+ if (heartbeatTimer) {
204
+ clearInterval(heartbeatTimer);
205
+ heartbeatTimer = null;
206
+ }
207
+ updateState('disconnected');
208
+ scheduleReconnect();
209
+ },
201
210
  onMessage: async (chatId, msgId, content) => {
202
211
  if (messageHandler) {
203
212
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wu529778790/open-im",
3
- "version": "1.9.4-beta.4",
3
+ "version": "1.9.4-beta.5",
4
4
  "description": "Multi-platform IM bridge for AI CLI tools (Claude, Codex, CodeBuddy)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",