@wu529778790/open-im 1.9.4-beta.4 → 1.9.4-beta.6
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.
- package/dist/dingtalk/event-handler.js +2 -2
- package/dist/feishu/event-handler.js +4 -4
- package/dist/platform/handle-ai-request.d.ts +2 -0
- package/dist/platform/handle-ai-request.js +2 -1
- package/dist/platform/handle-text-flow.js +2 -1
- package/dist/qq/event-handler.js +4 -3
- package/dist/queue/request-queue.d.ts +1 -1
- package/dist/queue/request-queue.js +6 -2
- package/dist/queue/request-queue.test.js +23 -5
- package/dist/shared/ai-task.d.ts +2 -0
- package/dist/shared/ai-task.js +9 -0
- package/dist/telegram/event-handler.js +2 -2
- package/dist/wework/event-handler.js +2 -2
- package/dist/workbuddy/centrifuge-client.d.ts +13 -0
- package/dist/workbuddy/centrifuge-client.js +73 -1
- package/dist/workbuddy/client.js +9 -0
- package/dist/workbuddy/event-handler.js +4 -4
- package/package.json +1 -1
|
@@ -199,9 +199,9 @@ export function setupDingTalkHandlers(config, sessionManager) {
|
|
|
199
199
|
async function enqueuePrompt(userId, chatId, prompt, dingtalkTarget) {
|
|
200
200
|
const workDir = sessionManager.getWorkDir(userId);
|
|
201
201
|
const convId = sessionManager.getConvId(userId);
|
|
202
|
-
return requestQueue.enqueue(userId, convId, prompt, async (nextPrompt) => {
|
|
202
|
+
return requestQueue.enqueue(userId, convId, prompt, async (nextPrompt, signal) => {
|
|
203
203
|
senderCtx.dingtalkTarget = dingtalkTarget;
|
|
204
|
-
await handleAIRequest({ userId, chatId, prompt: nextPrompt, workDir, convId });
|
|
204
|
+
await handleAIRequest({ userId, chatId, prompt: nextPrompt, workDir, convId, signal });
|
|
205
205
|
});
|
|
206
206
|
}
|
|
207
207
|
async function handleEvent(data) {
|
|
@@ -459,8 +459,8 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
459
459
|
});
|
|
460
460
|
const work = sessionManager.getWorkDir(senderId);
|
|
461
461
|
const convId = sessionManager.getConvId(senderId);
|
|
462
|
-
const enqueueResult = ctx.requestQueue.enqueue(senderId, convId, prompt, async (p) => {
|
|
463
|
-
await handleAIRequest({ userId: senderId, chatId, prompt: p, workDir: work, convId, replyToMessageId: messageId });
|
|
462
|
+
const enqueueResult = ctx.requestQueue.enqueue(senderId, convId, prompt, async (p, signal) => {
|
|
463
|
+
await handleAIRequest({ userId: senderId, chatId, prompt: p, workDir: work, convId, replyToMessageId: messageId, signal });
|
|
464
464
|
});
|
|
465
465
|
if (enqueueResult === 'rejected') {
|
|
466
466
|
sendTextReply(chatId, 'Request queue is full. Please try again later.').catch((sendErr) => {
|
|
@@ -507,8 +507,8 @@ export function setupFeishuHandlers(config, sessionManager) {
|
|
|
507
507
|
});
|
|
508
508
|
const workDir = sessionManager.getWorkDir(senderId);
|
|
509
509
|
const convId = sessionManager.getConvId(senderId);
|
|
510
|
-
const enqueueResult = ctx.requestQueue.enqueue(senderId, convId, prompt, async (p) => {
|
|
511
|
-
await handleAIRequest({ userId: senderId, chatId, prompt: p, workDir, convId, replyToMessageId: messageId });
|
|
510
|
+
const enqueueResult = ctx.requestQueue.enqueue(senderId, convId, prompt, async (p, signal) => {
|
|
511
|
+
await handleAIRequest({ userId: senderId, chatId, prompt: p, workDir, convId, replyToMessageId: messageId, signal });
|
|
512
512
|
});
|
|
513
513
|
if (enqueueResult === 'rejected') {
|
|
514
514
|
sendTextReply(chatId, 'Request queue is full. Please try again later.').catch((sendErr) => {
|
|
@@ -92,6 +92,8 @@ export interface HandleAIRequestParams {
|
|
|
92
92
|
workDir: string;
|
|
93
93
|
convId?: string;
|
|
94
94
|
replyToMessageId?: string;
|
|
95
|
+
/** AbortSignal from the request queue; fires on task timeout */
|
|
96
|
+
signal?: AbortSignal;
|
|
95
97
|
}
|
|
96
98
|
/**
|
|
97
99
|
* Creates a platform-specific handleAIRequest function.
|
|
@@ -29,7 +29,7 @@ const log = createLogger('PlatformAI');
|
|
|
29
29
|
export function createPlatformAIRequestHandler(deps) {
|
|
30
30
|
const { platform, config, sessionManager, sender, throttleMs, runningTasks, minContentDeltaChars, taskKeyBuilder, onThinkingToText, extraInit, taskCallbacks, taskCallbacksFactory, } = deps;
|
|
31
31
|
async function handleAIRequest(params) {
|
|
32
|
-
const { userId, chatId, prompt, workDir, convId, replyToMessageId } = params;
|
|
32
|
+
const { userId, chatId, prompt, workDir, convId, replyToMessageId, signal } = params;
|
|
33
33
|
log.info(`[${platform}] AI request: userId=${userId}, chatId=${chatId}, promptLength=${prompt.length}`);
|
|
34
34
|
// 1. Resolve AI command and adapter
|
|
35
35
|
const aiCommand = resolvePlatformAiCommand(config, platform);
|
|
@@ -156,6 +156,7 @@ export function createPlatformAIRequestHandler(deps) {
|
|
|
156
156
|
convId,
|
|
157
157
|
platform,
|
|
158
158
|
taskKey,
|
|
159
|
+
signal,
|
|
159
160
|
}, prompt, toolAdapter, mergedCallbacks);
|
|
160
161
|
}
|
|
161
162
|
catch (err) {
|
|
@@ -92,7 +92,7 @@ export async function handleTextFlow(params) {
|
|
|
92
92
|
const { requestQueue } = ctx;
|
|
93
93
|
const workDir = workDirOverride;
|
|
94
94
|
const convId = convIdOverride;
|
|
95
|
-
const enqueueResult = requestQueue.enqueue(userId, convId ?? '', text, async (prompt) => {
|
|
95
|
+
const enqueueResult = requestQueue.enqueue(userId, convId ?? '', text, async (prompt, signal) => {
|
|
96
96
|
await handleAIRequest({
|
|
97
97
|
userId,
|
|
98
98
|
chatId,
|
|
@@ -100,6 +100,7 @@ export async function handleTextFlow(params) {
|
|
|
100
100
|
workDir: workDir ?? '',
|
|
101
101
|
convId,
|
|
102
102
|
replyToMessageId,
|
|
103
|
+
signal,
|
|
103
104
|
});
|
|
104
105
|
});
|
|
105
106
|
if (enqueueResult === 'rejected') {
|
package/dist/qq/event-handler.js
CHANGED
|
@@ -167,7 +167,7 @@ export function setupQQHandlers(config, sessionManager) {
|
|
|
167
167
|
});
|
|
168
168
|
// Wrap factory handleAIRequest to match ClaudeRequestHandler signature
|
|
169
169
|
// (used by commandHandler.dispatch and handleTextFlow)
|
|
170
|
-
async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId) {
|
|
170
|
+
async function handleAIRequest(userId, chatId, prompt, workDir, convId, _threadCtx, replyToMessageId, signal) {
|
|
171
171
|
await factoryHandleAIRequest({
|
|
172
172
|
userId,
|
|
173
173
|
chatId,
|
|
@@ -175,6 +175,7 @@ export function setupQQHandlers(config, sessionManager) {
|
|
|
175
175
|
workDir,
|
|
176
176
|
convId,
|
|
177
177
|
replyToMessageId,
|
|
178
|
+
signal,
|
|
178
179
|
});
|
|
179
180
|
}
|
|
180
181
|
function cleanupRecentEvents(now) {
|
|
@@ -264,8 +265,8 @@ export function setupQQHandlers(config, sessionManager) {
|
|
|
264
265
|
setActiveChatId("qq", chatId);
|
|
265
266
|
setChatUser(chatId, userId, "qq");
|
|
266
267
|
// Enqueue attachment prompt
|
|
267
|
-
const enqueueResult = requestQueue.enqueue(userId, convId ?? '', attachmentPrompt, async (prompt) => {
|
|
268
|
-
await handleAIRequest(userId, chatId, prompt, workDir, convId, undefined, event.id);
|
|
268
|
+
const enqueueResult = requestQueue.enqueue(userId, convId ?? '', attachmentPrompt, async (prompt, signal) => {
|
|
269
|
+
await handleAIRequest(userId, chatId, prompt, workDir, convId, undefined, event.id, signal);
|
|
269
270
|
});
|
|
270
271
|
if (enqueueResult === "rejected") {
|
|
271
272
|
await sendTextReply(chatId, "Request queue is full. Please try again later.");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export type EnqueueResult = 'running' | 'queued' | 'rejected';
|
|
2
2
|
export declare class RequestQueue {
|
|
3
3
|
private queues;
|
|
4
|
-
enqueue(userId: string, convId: string, prompt: string, execute: (prompt: string) => Promise<void>): EnqueueResult;
|
|
4
|
+
enqueue(userId: string, convId: string, prompt: string, execute: (prompt: string, signal: AbortSignal) => Promise<void>): EnqueueResult;
|
|
5
5
|
/** 清除指定用户会话的所有排队任务(不中止正在运行的任务) */
|
|
6
6
|
clear(userId: string, convId: string): number;
|
|
7
7
|
private run;
|
|
@@ -35,12 +35,16 @@ export class RequestQueue {
|
|
|
35
35
|
return cleared;
|
|
36
36
|
}
|
|
37
37
|
async run(key, prompt, execute) {
|
|
38
|
+
const controller = new AbortController();
|
|
38
39
|
let timer;
|
|
39
40
|
try {
|
|
40
41
|
const timeoutPromise = new Promise((_, reject) => {
|
|
41
|
-
timer = setTimeout(() =>
|
|
42
|
+
timer = setTimeout(() => {
|
|
43
|
+
controller.abort();
|
|
44
|
+
reject(new Error(`Task timed out after ${TASK_TIMEOUT_MS / 1000}s`));
|
|
45
|
+
}, TASK_TIMEOUT_MS);
|
|
42
46
|
});
|
|
43
|
-
await Promise.race([execute(prompt), timeoutPromise]);
|
|
47
|
+
await Promise.race([execute(prompt, controller.signal), timeoutPromise]);
|
|
44
48
|
}
|
|
45
49
|
catch (err) {
|
|
46
50
|
log.error(`Error executing task for ${key}:`, err);
|
|
@@ -8,7 +8,7 @@ describe('RequestQueue', () => {
|
|
|
8
8
|
expect(result).toBe('running');
|
|
9
9
|
// Allow microtask queue to settle
|
|
10
10
|
await new Promise((r) => setTimeout(r, 10));
|
|
11
|
-
expect(execute).toHaveBeenCalledWith('hello');
|
|
11
|
+
expect(execute).toHaveBeenCalledWith('hello', expect.any(AbortSignal));
|
|
12
12
|
});
|
|
13
13
|
it('returns "queued" when a task is already running', () => {
|
|
14
14
|
const queue = new RequestQueue();
|
|
@@ -37,11 +37,11 @@ describe('RequestQueue', () => {
|
|
|
37
37
|
queue.enqueue('user1', 'conv1', 'first', execute);
|
|
38
38
|
queue.enqueue('user1', 'conv1', 'second', execute);
|
|
39
39
|
expect(execute).toHaveBeenCalledTimes(1);
|
|
40
|
-
expect(execute).toHaveBeenCalledWith('first');
|
|
40
|
+
expect(execute).toHaveBeenCalledWith('first', expect.any(AbortSignal));
|
|
41
41
|
resolveFirst();
|
|
42
42
|
await new Promise((r) => setTimeout(r, 20));
|
|
43
43
|
expect(execute).toHaveBeenCalledTimes(2);
|
|
44
|
-
expect(execute).toHaveBeenCalledWith('second');
|
|
44
|
+
expect(execute).toHaveBeenCalledWith('second', expect.any(AbortSignal));
|
|
45
45
|
});
|
|
46
46
|
it('isolates queues per user:convId', () => {
|
|
47
47
|
const queue = new RequestQueue();
|
|
@@ -90,7 +90,25 @@ describe('RequestQueue', () => {
|
|
|
90
90
|
queue.enqueue('user1', 'conv1', 'second', execute);
|
|
91
91
|
await new Promise((r) => setTimeout(r, 20));
|
|
92
92
|
expect(execute).toHaveBeenCalledTimes(2);
|
|
93
|
-
expect(execute).toHaveBeenCalledWith('first');
|
|
94
|
-
expect(execute).toHaveBeenCalledWith('second');
|
|
93
|
+
expect(execute).toHaveBeenCalledWith('first', expect.any(AbortSignal));
|
|
94
|
+
expect(execute).toHaveBeenCalledWith('second', expect.any(AbortSignal));
|
|
95
|
+
});
|
|
96
|
+
it('aborts the AbortSignal on timeout', async () => {
|
|
97
|
+
vi.useFakeTimers();
|
|
98
|
+
const queue = new RequestQueue();
|
|
99
|
+
let receivedSignal;
|
|
100
|
+
const execute = vi.fn().mockImplementation(async (_prompt, signal) => {
|
|
101
|
+
receivedSignal = signal;
|
|
102
|
+
// Never resolve — simulates a stuck task
|
|
103
|
+
await new Promise(() => { });
|
|
104
|
+
});
|
|
105
|
+
queue.enqueue('user1', 'conv1', 'hello', execute);
|
|
106
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
107
|
+
expect(receivedSignal?.aborted).toBe(false);
|
|
108
|
+
// Advance past timeout
|
|
109
|
+
vi.advanceTimersByTime(10 * 60 * 1000 + 1);
|
|
110
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
111
|
+
expect(receivedSignal?.aborted).toBe(true);
|
|
112
|
+
vi.useRealTimers();
|
|
95
113
|
});
|
|
96
114
|
});
|
package/dist/shared/ai-task.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface TaskContext {
|
|
|
17
17
|
threadId?: string;
|
|
18
18
|
platform: string;
|
|
19
19
|
taskKey: string;
|
|
20
|
+
/** AbortSignal from the request queue; fires on task timeout to abort the running SDK session */
|
|
21
|
+
signal?: AbortSignal;
|
|
20
22
|
}
|
|
21
23
|
export interface TaskAdapter {
|
|
22
24
|
streamUpdate(content: string, toolNote?: string): void;
|
package/dist/shared/ai-task.js
CHANGED
|
@@ -255,5 +255,14 @@ export function runAITask(deps, ctx, prompt, toolAdapter, platformAdapter) {
|
|
|
255
255
|
return;
|
|
256
256
|
}
|
|
257
257
|
platformAdapter.onTaskReady(taskState);
|
|
258
|
+
// Wire queue abort signal to the running task's abort handle
|
|
259
|
+
if (ctx.signal) {
|
|
260
|
+
if (ctx.signal.aborted) {
|
|
261
|
+
taskState.handle.abort();
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
ctx.signal.addEventListener('abort', () => taskState.handle.abort(), { once: true });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
258
267
|
});
|
|
259
268
|
}
|
|
@@ -254,8 +254,8 @@ export function setupTelegramHandlers(bot, config, sessionManager) {
|
|
|
254
254
|
});
|
|
255
255
|
const workDir = sessionManager.getWorkDir(userId);
|
|
256
256
|
const convId = sessionManager.getConvId(userId);
|
|
257
|
-
return requestQueue.enqueue(userId, convId, prompt, async (nextPrompt) => {
|
|
258
|
-
await handleAIRequest({ userId, chatId, prompt: nextPrompt, workDir, convId });
|
|
257
|
+
return requestQueue.enqueue(userId, convId, prompt, async (nextPrompt, signal) => {
|
|
258
|
+
await handleAIRequest({ userId, chatId, prompt: nextPrompt, workDir, convId, signal });
|
|
259
259
|
});
|
|
260
260
|
}
|
|
261
261
|
bot.on("callback_query", async (ctx) => {
|
|
@@ -240,8 +240,8 @@ export function setupWeWorkHandlers(config, sessionManager) {
|
|
|
240
240
|
async function enqueuePrompt(userId, chatId, prompt, reqId) {
|
|
241
241
|
const workDir = sessionManager.getWorkDir(userId);
|
|
242
242
|
const convId = sessionManager.getConvId(userId);
|
|
243
|
-
const enqueueResult = ctx.requestQueue.enqueue(userId, convId, prompt, async (nextPrompt) => {
|
|
244
|
-
await handleAIRequest({ userId, chatId, prompt: nextPrompt, workDir, convId, replyToMessageId: undefined });
|
|
243
|
+
const enqueueResult = ctx.requestQueue.enqueue(userId, convId, prompt, async (nextPrompt, signal) => {
|
|
244
|
+
await handleAIRequest({ userId, chatId, prompt: nextPrompt, workDir, convId, replyToMessageId: undefined, signal });
|
|
245
245
|
});
|
|
246
246
|
if (enqueueResult === 'rejected') {
|
|
247
247
|
await sendTextReply(chatId, 'Request queue is full. Please try again later.', reqId);
|
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/workbuddy/client.js
CHANGED
|
@@ -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 {
|
|
@@ -83,7 +83,7 @@ export function setupWorkBuddyHandlers(config, sessionManager) {
|
|
|
83
83
|
extraInit,
|
|
84
84
|
});
|
|
85
85
|
// WorkBuddy-specific wrapper that captures msgId
|
|
86
|
-
async function handleAIRequest(userId, chatId, msgId, prompt, workDir, convId) {
|
|
86
|
+
async function handleAIRequest(userId, chatId, msgId, prompt, workDir, convId, signal) {
|
|
87
87
|
log.info(`[AI_REQUEST] userId=${userId}, chatId=${chatId}, msgId=${msgId}, promptLength=${prompt.length}`);
|
|
88
88
|
// WorkBuddy uses incoming msgId as taskKey (no thinking message needed)
|
|
89
89
|
const taskKey = `${userId}:${msgId}`;
|
|
@@ -104,7 +104,7 @@ export function setupWorkBuddyHandlers(config, sessionManager) {
|
|
|
104
104
|
log.info(`[handleAIRequest] Running ${aiCommand} for user ${userId}, sessionId=${sessionId ?? 'new'}`);
|
|
105
105
|
// Set up task tracking key mapping
|
|
106
106
|
taskKeyByChatId.set(chatId, taskKey);
|
|
107
|
-
await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: 'workbuddy', taskKey }, prompt, toolAdapter, {
|
|
107
|
+
await runAITask({ config, sessionManager }, { userId, chatId, workDir, sessionId, convId, platform: 'workbuddy', taskKey, signal }, prompt, toolAdapter, {
|
|
108
108
|
throttleMs: WORKBUDDY_THROTTLE_MS,
|
|
109
109
|
streamUpdate: async (content) => {
|
|
110
110
|
log.debug(`Stream update (not sent): ${content.substring(0, 50)}...`);
|
|
@@ -172,9 +172,9 @@ export function setupWorkBuddyHandlers(config, sessionManager) {
|
|
|
172
172
|
if (!text) {
|
|
173
173
|
return;
|
|
174
174
|
}
|
|
175
|
-
const enqueueResult = ctx.requestQueue.enqueue(userId, convId, text, async (nextPrompt) => {
|
|
175
|
+
const enqueueResult = ctx.requestQueue.enqueue(userId, convId, text, async (nextPrompt, signal) => {
|
|
176
176
|
log.info(`Executing AI request for: ${nextPrompt}`);
|
|
177
|
-
await handleAIRequest(userId, chatId, msgId, nextPrompt, workDir, convId);
|
|
177
|
+
await handleAIRequest(userId, chatId, msgId, nextPrompt, workDir, convId, signal);
|
|
178
178
|
});
|
|
179
179
|
if (enqueueResult === 'rejected') {
|
|
180
180
|
await sendErrorReply(null, chatId, 'Request queue is full. Please try again later.', msgId);
|