@gonzih/cc-tg 0.6.3 → 0.6.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.
- package/dist/cc-agent-events.d.ts +18 -0
- package/dist/cc-agent-events.js +225 -21
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Controlled via CC_AGENT_EVENTS_ENABLED env var (default: true).
|
|
11
11
|
* Requires CC_AGENT_NOTIFY_CHAT_ID to send Telegram notifications.
|
|
12
12
|
*/
|
|
13
|
+
import { Redis } from "ioredis";
|
|
13
14
|
export interface JobEvent {
|
|
14
15
|
jobId: string;
|
|
15
16
|
status: "done" | "failed" | "interrupted" | "running" | "cancelled";
|
|
@@ -42,6 +43,7 @@ export interface HandlerDeps {
|
|
|
42
43
|
readJobOutput: (jobId: string) => Promise<string[]>;
|
|
43
44
|
readCoordinatorPlan: (jobId: string) => Promise<CoordinatorPlan | null>;
|
|
44
45
|
getRunningJobCount: () => Promise<number>;
|
|
46
|
+
getActiveChatIds: () => Promise<number[]>;
|
|
45
47
|
}
|
|
46
48
|
export declare function buildDecisionPrompt(event: JobEvent, last40lines: string[], coordinatorPlan: CoordinatorPlan | null): string;
|
|
47
49
|
export declare function parseDecision(raw: string): DecisionResult;
|
|
@@ -55,6 +57,12 @@ export declare function defaultSpawnFollowupAgent(repoUrl: string, task: string)
|
|
|
55
57
|
export declare function defaultReadJobOutput(jobId: string): Promise<string[]>;
|
|
56
58
|
export declare function defaultReadCoordinatorPlan(jobId: string): Promise<CoordinatorPlan | null>;
|
|
57
59
|
export declare function defaultGetRunningJobCount(): Promise<number>;
|
|
60
|
+
/**
|
|
61
|
+
* Returns chat IDs to notify about job events.
|
|
62
|
+
* Reads unique chatIds from the cron jobs file (same users who set up cron jobs).
|
|
63
|
+
* Falls back to CC_AGENT_NOTIFY_CHAT_ID env var for backward compatibility.
|
|
64
|
+
*/
|
|
65
|
+
export declare function defaultGetActiveChatIds(): Promise<number[]>;
|
|
58
66
|
/**
|
|
59
67
|
* Write a coordinator plan for a job, so cc-tg knows what follow-up to spawn.
|
|
60
68
|
* Call this when spawning a job that has a planned follow-up.
|
|
@@ -72,6 +80,16 @@ export declare function writeCoordinatorPlan(jobId: string, plan: {
|
|
|
72
80
|
* Exported for testability — production code passes defaultDeps.
|
|
73
81
|
*/
|
|
74
82
|
export declare function handleJobEvent(message: string, deps: HandlerDeps): Promise<void>;
|
|
83
|
+
/** Parse flat key-value field array from a Redis Stream entry into a record. */
|
|
84
|
+
export declare function parseStreamFields(fields: string[]): Record<string, string>;
|
|
85
|
+
/** Convert stream entry fields to a JobEvent JSON string for handleJobEvent. */
|
|
86
|
+
export declare function streamEntryToMessage(fields: Record<string, string>): string | null;
|
|
87
|
+
/**
|
|
88
|
+
* Replay events from the Redis Stream that were missed since last-seen ID.
|
|
89
|
+
* Uses `cca:event-stream:last-id:{botName}` in Redis to track position.
|
|
90
|
+
* Exported for testability — pass a real or mock Redis instance.
|
|
91
|
+
*/
|
|
92
|
+
export declare function replayStreamEvents(redis: Redis, deps: HandlerDeps, botName?: string): Promise<void>;
|
|
75
93
|
/**
|
|
76
94
|
* Connect to Redis and subscribe to cca:events.
|
|
77
95
|
* Reconnects automatically on disconnect.
|
package/dist/cc-agent-events.js
CHANGED
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
* Controlled via CC_AGENT_EVENTS_ENABLED env var (default: true).
|
|
11
11
|
* Requires CC_AGENT_NOTIFY_CHAT_ID to send Telegram notifications.
|
|
12
12
|
*/
|
|
13
|
+
import { readFileSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
13
15
|
import { Redis } from "ioredis";
|
|
14
16
|
import TelegramBot from "node-telegram-bot-api";
|
|
17
|
+
const STREAM_KEY = "cca:event-stream";
|
|
15
18
|
import { ClaudeProcess, extractText } from "./claude.js";
|
|
16
19
|
function log(level, ...args) {
|
|
17
20
|
const fn = level === "error"
|
|
@@ -56,11 +59,23 @@ Reply in JSON:
|
|
|
56
59
|
"followup": { "repo_url": "...", "task": "..." } | null
|
|
57
60
|
}`;
|
|
58
61
|
}
|
|
62
|
+
function extractJson(text) {
|
|
63
|
+
// Strip ```json ... ``` fences
|
|
64
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
65
|
+
if (fenced)
|
|
66
|
+
return fenced[1].trim();
|
|
67
|
+
// Find first { ... } block
|
|
68
|
+
const start = text.indexOf("{");
|
|
69
|
+
const end = text.lastIndexOf("}");
|
|
70
|
+
if (start !== -1 && end !== -1)
|
|
71
|
+
return text.slice(start, end + 1);
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
59
74
|
export function parseDecision(raw) {
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
75
|
+
const extracted = extractJson(raw);
|
|
76
|
+
if (!extracted)
|
|
62
77
|
throw new Error(`No JSON found in Claude response: ${raw.slice(0, 200)}`);
|
|
63
|
-
const parsed = JSON.parse(
|
|
78
|
+
const parsed = JSON.parse(extracted);
|
|
64
79
|
if (!["NOTIFY_ONLY", "SPAWN_FOLLOWUP", "SILENT"].includes(parsed.action)) {
|
|
65
80
|
throw new Error(`Unknown action: ${parsed.action}`);
|
|
66
81
|
}
|
|
@@ -83,15 +98,6 @@ function formatFailureMessage(event) {
|
|
|
83
98
|
const repoShort = event.repoUrl.replace(/^https?:\/\/github\.com\//, "");
|
|
84
99
|
return `✗ ${event.title} failed\n${repoShort} — exit 1\nLast line: ${lastLine}`;
|
|
85
100
|
}
|
|
86
|
-
function getChatId() {
|
|
87
|
-
const chatIdStr = process.env.CC_AGENT_NOTIFY_CHAT_ID;
|
|
88
|
-
if (!chatIdStr)
|
|
89
|
-
return null;
|
|
90
|
-
const chatId = Number(chatIdStr);
|
|
91
|
-
if (isNaN(chatId))
|
|
92
|
-
return null;
|
|
93
|
-
return chatId;
|
|
94
|
-
}
|
|
95
101
|
/**
|
|
96
102
|
* Ask Claude to make a decision about a completed job.
|
|
97
103
|
* Returns the raw text response from Claude.
|
|
@@ -223,6 +229,36 @@ export async function defaultReadCoordinatorPlan(jobId) {
|
|
|
223
229
|
export async function defaultGetRunningJobCount() {
|
|
224
230
|
return 0;
|
|
225
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Returns chat IDs to notify about job events.
|
|
234
|
+
* Reads unique chatIds from the cron jobs file (same users who set up cron jobs).
|
|
235
|
+
* Falls back to CC_AGENT_NOTIFY_CHAT_ID env var for backward compatibility.
|
|
236
|
+
*/
|
|
237
|
+
export async function defaultGetActiveChatIds() {
|
|
238
|
+
const ids = new Set();
|
|
239
|
+
// Backward compat: explicit env var
|
|
240
|
+
const chatIdStr = process.env.CC_AGENT_NOTIFY_CHAT_ID;
|
|
241
|
+
if (chatIdStr) {
|
|
242
|
+
const chatId = Number(chatIdStr);
|
|
243
|
+
if (!isNaN(chatId))
|
|
244
|
+
ids.add(chatId);
|
|
245
|
+
}
|
|
246
|
+
// Read chatIds from cron jobs persistence file
|
|
247
|
+
try {
|
|
248
|
+
const cwd = process.env.CWD ?? process.cwd();
|
|
249
|
+
const cronFile = join(cwd, ".cc-tg", "crons.json");
|
|
250
|
+
const raw = readFileSync(cronFile, "utf-8");
|
|
251
|
+
const jobs = JSON.parse(raw);
|
|
252
|
+
for (const job of jobs) {
|
|
253
|
+
if (typeof job.chatId === "number")
|
|
254
|
+
ids.add(job.chatId);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// file doesn't exist or parse error — ignore
|
|
259
|
+
}
|
|
260
|
+
return Array.from(ids);
|
|
261
|
+
}
|
|
226
262
|
/**
|
|
227
263
|
* Write a coordinator plan for a job, so cc-tg knows what follow-up to spawn.
|
|
228
264
|
* Call this when spawning a job that has a planned follow-up.
|
|
@@ -280,12 +316,49 @@ export async function handleJobEvent(message, deps) {
|
|
|
280
316
|
catch (err) {
|
|
281
317
|
log("warn", "Failed to read coordinator plan:", err.message);
|
|
282
318
|
}
|
|
319
|
+
// Fast path: coordinator plan has explicit next step — spawn directly, no Claude needed
|
|
320
|
+
// This eliminates JSON truncation issues when Claude regenerates long task strings.
|
|
321
|
+
if (coordinatorPlan?.nextStep) {
|
|
322
|
+
log("info", `Fast path: coordinator plan nextStep found for job ${event.jobId}`);
|
|
323
|
+
const { repo_url, task } = coordinatorPlan.nextStep;
|
|
324
|
+
let fpChatIds = [];
|
|
325
|
+
try {
|
|
326
|
+
fpChatIds = await deps.getActiveChatIds();
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
log("warn", "Fast path: failed to get active chat IDs:", err.message);
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
await deps.spawnFollowupAgent(repo_url, task);
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
log("error", "Fast path: spawnFollowupAgent failed:", err.message);
|
|
336
|
+
}
|
|
337
|
+
if (fpChatIds.length > 0) {
|
|
338
|
+
const scoreStr = event.score !== undefined ? ` (score: ${event.score})` : "";
|
|
339
|
+
const repoShort = repo_url.split("/").pop() ?? repo_url;
|
|
340
|
+
const msg = `✓ ${event.title} done${scoreStr}\n→ spawned: ${repoShort}`;
|
|
341
|
+
for (const chatId of fpChatIds) {
|
|
342
|
+
try {
|
|
343
|
+
await deps.sendTelegramMessage(chatId, msg);
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
log("error", "Fast path: sendTelegramMessage failed:", err.message);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
283
352
|
let decision;
|
|
353
|
+
let rawResponse = "";
|
|
284
354
|
try {
|
|
285
|
-
|
|
355
|
+
rawResponse = await deps.askClaude(buildDecisionPrompt(event, last40lines, coordinatorPlan));
|
|
286
356
|
decision = parseDecision(rawResponse);
|
|
287
357
|
}
|
|
288
358
|
catch (err) {
|
|
359
|
+
if (rawResponse) {
|
|
360
|
+
log("error", "[cc-agent-events] Claude raw response:", rawResponse.slice(0, 200));
|
|
361
|
+
}
|
|
289
362
|
log("error", "Claude decision failed, falling back to NOTIFY_ONLY:", err.message);
|
|
290
363
|
const fallbackMsg = event.status === "failed"
|
|
291
364
|
? formatFailureMessage(event)
|
|
@@ -293,16 +366,24 @@ export async function handleJobEvent(message, deps) {
|
|
|
293
366
|
decision = { action: "NOTIFY_ONLY", message: fallbackMsg };
|
|
294
367
|
}
|
|
295
368
|
log("info", `Decision: ${decision.action} for job ${event.jobId}`);
|
|
296
|
-
|
|
369
|
+
let chatIds = [];
|
|
370
|
+
try {
|
|
371
|
+
chatIds = await deps.getActiveChatIds();
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
log("warn", "Failed to get active chat IDs:", err.message);
|
|
375
|
+
}
|
|
297
376
|
try {
|
|
298
377
|
if (decision.action === "NOTIFY_ONLY") {
|
|
299
|
-
if (
|
|
300
|
-
log("warn", "NOTIFY_ONLY:
|
|
378
|
+
if (chatIds.length === 0) {
|
|
379
|
+
log("warn", "NOTIFY_ONLY: no active chat IDs, skipping notification");
|
|
301
380
|
return;
|
|
302
381
|
}
|
|
303
382
|
const msg = decision.message
|
|
304
383
|
?? (event.status === "failed" ? formatFailureMessage(event) : `Job completed: ${event.title}`);
|
|
305
|
-
|
|
384
|
+
for (const chatId of chatIds) {
|
|
385
|
+
await deps.sendTelegramMessage(chatId, msg);
|
|
386
|
+
}
|
|
306
387
|
}
|
|
307
388
|
else if (decision.action === "SPAWN_FOLLOWUP") {
|
|
308
389
|
if (!decision.followup) {
|
|
@@ -311,14 +392,16 @@ export async function handleJobEvent(message, deps) {
|
|
|
311
392
|
}
|
|
312
393
|
await deps.spawnFollowupAgent(decision.followup.repo_url, decision.followup.task);
|
|
313
394
|
// Send Telegram notification about the spawn
|
|
314
|
-
if (
|
|
395
|
+
if (chatIds.length > 0) {
|
|
315
396
|
let runningCount = 0;
|
|
316
397
|
try {
|
|
317
398
|
runningCount = await deps.getRunningJobCount();
|
|
318
399
|
}
|
|
319
400
|
catch { }
|
|
320
401
|
const spawnMsg = formatSpawnMessage(event, decision.followup, runningCount);
|
|
321
|
-
|
|
402
|
+
for (const chatId of chatIds) {
|
|
403
|
+
await deps.sendTelegramMessage(chatId, spawnMsg);
|
|
404
|
+
}
|
|
322
405
|
}
|
|
323
406
|
}
|
|
324
407
|
else {
|
|
@@ -330,6 +413,80 @@ export async function handleJobEvent(message, deps) {
|
|
|
330
413
|
log("error", `Action ${decision.action} failed:`, err.message);
|
|
331
414
|
}
|
|
332
415
|
}
|
|
416
|
+
/** Parse flat key-value field array from a Redis Stream entry into a record. */
|
|
417
|
+
export function parseStreamFields(fields) {
|
|
418
|
+
const obj = {};
|
|
419
|
+
for (let i = 0; i + 1 < fields.length; i += 2) {
|
|
420
|
+
obj[fields[i]] = fields[i + 1];
|
|
421
|
+
}
|
|
422
|
+
return obj;
|
|
423
|
+
}
|
|
424
|
+
/** Convert stream entry fields to a JobEvent JSON string for handleJobEvent. */
|
|
425
|
+
export function streamEntryToMessage(fields) {
|
|
426
|
+
try {
|
|
427
|
+
const score = fields["score"] !== undefined && fields["score"] !== ""
|
|
428
|
+
? Number(fields["score"])
|
|
429
|
+
: undefined;
|
|
430
|
+
const event = {
|
|
431
|
+
jobId: fields["jobId"] ?? "",
|
|
432
|
+
status: (fields["status"] ?? "done"),
|
|
433
|
+
title: fields["title"] ?? "",
|
|
434
|
+
repoUrl: fields["repoUrl"] ?? "",
|
|
435
|
+
lastLines: JSON.parse(fields["lastLines"] ?? "[]"),
|
|
436
|
+
score,
|
|
437
|
+
timestamp: Number(fields["timestamp"] ?? Date.now()),
|
|
438
|
+
};
|
|
439
|
+
return JSON.stringify(event);
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Replay events from the Redis Stream that were missed since last-seen ID.
|
|
447
|
+
* Uses `cca:event-stream:last-id:{botName}` in Redis to track position.
|
|
448
|
+
* Exported for testability — pass a real or mock Redis instance.
|
|
449
|
+
*/
|
|
450
|
+
export async function replayStreamEvents(redis, deps, botName) {
|
|
451
|
+
const name = botName ?? (process.env.CC_TG_BOT_NAME ?? "cc-tg");
|
|
452
|
+
const lastIdKey = `cca:event-stream:last-id:${name}`;
|
|
453
|
+
let lastId = "0";
|
|
454
|
+
try {
|
|
455
|
+
lastId = (await redis.get(lastIdKey)) ?? "0";
|
|
456
|
+
}
|
|
457
|
+
catch (err) {
|
|
458
|
+
log("warn", "replayStreamEvents: failed to read last-id:", err.message);
|
|
459
|
+
}
|
|
460
|
+
let results = null;
|
|
461
|
+
try {
|
|
462
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
463
|
+
results = (await redis.xread("COUNT", 20, "STREAMS", STREAM_KEY, lastId));
|
|
464
|
+
}
|
|
465
|
+
catch (err) {
|
|
466
|
+
log("warn", "replayStreamEvents: xread failed:", err.message);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (!results || results.length === 0)
|
|
470
|
+
return;
|
|
471
|
+
log("info", `Replaying missed stream events from last-id=${lastId}`);
|
|
472
|
+
for (const [, entries] of results) {
|
|
473
|
+
for (const [id, fields] of entries) {
|
|
474
|
+
const message = streamEntryToMessage(parseStreamFields(fields));
|
|
475
|
+
if (message) {
|
|
476
|
+
await handleJobEvent(message, deps).catch((err) => {
|
|
477
|
+
log("error", `replayStreamEvents: handleJobEvent error for entry ${id}:`, err.message);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
await redis.set(lastIdKey, id);
|
|
482
|
+
}
|
|
483
|
+
catch (err) {
|
|
484
|
+
log("warn", "replayStreamEvents: failed to update last-id:", err.message);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
log("info", "Stream replay complete.");
|
|
489
|
+
}
|
|
333
490
|
function makeDefaultDeps() {
|
|
334
491
|
return {
|
|
335
492
|
askClaude: defaultAskClaude,
|
|
@@ -338,6 +495,7 @@ function makeDefaultDeps() {
|
|
|
338
495
|
readJobOutput: defaultReadJobOutput,
|
|
339
496
|
readCoordinatorPlan: defaultReadCoordinatorPlan,
|
|
340
497
|
getRunningJobCount: defaultGetRunningJobCount,
|
|
498
|
+
getActiveChatIds: defaultGetActiveChatIds,
|
|
341
499
|
};
|
|
342
500
|
}
|
|
343
501
|
let subscriberClient = null;
|
|
@@ -355,10 +513,19 @@ export async function connectEventSubscriber() {
|
|
|
355
513
|
}
|
|
356
514
|
async function connectWithBackoff(attempt) {
|
|
357
515
|
const delay = Math.min(5_000 * Math.pow(2, attempt), 60_000);
|
|
358
|
-
const
|
|
516
|
+
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
|
|
517
|
+
const botName = process.env.CC_TG_BOT_NAME ?? "cc-tg";
|
|
518
|
+
const lastIdKey = `cca:event-stream:last-id:${botName}`;
|
|
519
|
+
// Pub/sub subscriber client — enters subscriber mode after subscribe()
|
|
520
|
+
const sub = new Redis(redisUrl, {
|
|
359
521
|
lazyConnect: true,
|
|
360
522
|
enableOfflineQueue: false,
|
|
361
523
|
});
|
|
524
|
+
// Regular command client — stays in normal mode for xread/get/set
|
|
525
|
+
const reg = new Redis(redisUrl, {
|
|
526
|
+
lazyConnect: true,
|
|
527
|
+
enableOfflineQueue: true,
|
|
528
|
+
});
|
|
362
529
|
subscriberClient = sub;
|
|
363
530
|
sub.on("error", (err) => {
|
|
364
531
|
log("warn", "subscriber error, reconnecting...", err.message);
|
|
@@ -366,8 +533,15 @@ async function connectWithBackoff(attempt) {
|
|
|
366
533
|
sub.disconnect();
|
|
367
534
|
}
|
|
368
535
|
catch { }
|
|
536
|
+
try {
|
|
537
|
+
reg.disconnect();
|
|
538
|
+
}
|
|
539
|
+
catch { }
|
|
369
540
|
setTimeout(() => connectWithBackoff(0), 5_000);
|
|
370
541
|
});
|
|
542
|
+
reg.on("error", (err) => {
|
|
543
|
+
log("warn", "regular client error (non-fatal):", err.message);
|
|
544
|
+
});
|
|
371
545
|
try {
|
|
372
546
|
await sub.connect();
|
|
373
547
|
}
|
|
@@ -380,11 +554,36 @@ async function connectWithBackoff(attempt) {
|
|
|
380
554
|
setTimeout(() => connectWithBackoff(attempt + 1), delay);
|
|
381
555
|
return;
|
|
382
556
|
}
|
|
557
|
+
// Connect regular client (best-effort — stream replay is non-critical)
|
|
558
|
+
try {
|
|
559
|
+
await reg.connect();
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
log("warn", "Regular Redis client connect failed (stream replay skipped):", err.message);
|
|
563
|
+
}
|
|
383
564
|
const deps = makeDefaultDeps();
|
|
565
|
+
// Replay events missed during downtime, then mark current time as last-id
|
|
566
|
+
// Must happen BEFORE sub.subscribe() because subscribe() puts sub in subscriber mode
|
|
567
|
+
try {
|
|
568
|
+
await replayStreamEvents(reg, deps, botName);
|
|
569
|
+
}
|
|
570
|
+
catch (err) {
|
|
571
|
+
log("warn", "Stream replay failed, continuing:", err.message);
|
|
572
|
+
}
|
|
573
|
+
// Mark current timestamp so next restart only replays events after now
|
|
574
|
+
try {
|
|
575
|
+
await reg.set(lastIdKey, `${Date.now()}-0`);
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
// Non-fatal
|
|
579
|
+
}
|
|
384
580
|
sub.on("message", (channel, message) => {
|
|
385
581
|
if (channel !== "cca:events")
|
|
386
582
|
return;
|
|
387
|
-
handleJobEvent(message, deps).
|
|
583
|
+
handleJobEvent(message, deps).then(() => {
|
|
584
|
+
// Advance stream last-id so next restart doesn't re-replay this event
|
|
585
|
+
reg.set(lastIdKey, `${Date.now()}-0`).catch(() => { });
|
|
586
|
+
}).catch((err) => {
|
|
388
587
|
log("error", "handleJobEvent uncaught:", err.message);
|
|
389
588
|
});
|
|
390
589
|
});
|
|
@@ -398,6 +597,10 @@ async function connectWithBackoff(attempt) {
|
|
|
398
597
|
sub.disconnect();
|
|
399
598
|
}
|
|
400
599
|
catch { }
|
|
600
|
+
try {
|
|
601
|
+
reg.disconnect();
|
|
602
|
+
}
|
|
603
|
+
catch { }
|
|
401
604
|
setTimeout(() => connectWithBackoff(attempt + 1), delay);
|
|
402
605
|
return;
|
|
403
606
|
}
|
|
@@ -406,6 +609,7 @@ async function connectWithBackoff(attempt) {
|
|
|
406
609
|
try {
|
|
407
610
|
await sub.unsubscribe("cca:events");
|
|
408
611
|
sub.disconnect();
|
|
612
|
+
reg.disconnect();
|
|
409
613
|
}
|
|
410
614
|
catch { }
|
|
411
615
|
};
|