grok-telegram-bot 2.3.0 → 2.3.1
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/CHANGELOG.md +18 -0
- package/package.json +1 -1
- package/src/bot/account-rotator.ts +61 -2
- package/src/bot/handlers/accounts.ts +4 -4
- package/src/bot/reauth-controller.ts +2 -2
- package/src/bot/session-runtime.ts +91 -6
- package/src/grok/client.ts +15 -4
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,23 @@ The latest section is published verbatim as the GitHub Release notes by
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [2.3.1] - 2026-07-19
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **Stable account rotation across concurrent chats.** Account selection is now
|
|
17
|
+
serialized around the single shared Grok CLI process. Chats reuse the working
|
|
18
|
+
account selected by another session and create their own ACP session instead
|
|
19
|
+
of starting competing rotation loops that repeatedly restart the agent.
|
|
20
|
+
- **Session failures no longer condemn valid accounts.** `unknown session id`,
|
|
21
|
+
agent-restart/process-exit, and headless-auth initialization errors now trigger
|
|
22
|
+
a session rebind/retry on the active account rather than quarantining or
|
|
23
|
+
cycling functional logins.
|
|
24
|
+
- **Reliable restart propagation.** Credential imports, manual switches,
|
|
25
|
+
reauthentication, and automatic rotation notify every chat runtime after the
|
|
26
|
+
shared process restarts; new session binding waits until candidate validation
|
|
27
|
+
completes.
|
|
28
|
+
|
|
12
29
|
## [2.3.0] - 2026-07-17
|
|
13
30
|
|
|
14
31
|
### Added
|
|
@@ -761,6 +778,7 @@ from a single chat and switch between them, on a redesigned, compact menu.
|
|
|
761
778
|
diffs, MarkdownV2 rendering, scheduled tasks, multi-image prompts, and a
|
|
762
779
|
cross-platform 24/7 background service.
|
|
763
780
|
|
|
781
|
+
[2.3.1]: https://github.com/artickc/grok-telegram-bot/releases/tag/v2.3.1
|
|
764
782
|
[2.3.0]: https://github.com/artickc/grok-telegram-bot/releases/tag/v2.3.0
|
|
765
783
|
[2.2.4]: https://github.com/artickc/grok-telegram-bot/releases/tag/v2.2.4
|
|
766
784
|
[2.2.3]: https://github.com/artickc/grok-telegram-bot/releases/tag/v2.2.3
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "grok-telegram-bot",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "Control the official Grok Build CLI from Telegram over the Agent Client Protocol (ACP). Sign in with your xAI account, switch projects, resume sessions, stream responses with diffs, queue follow-ups, manage multiple sign-ins, and run 24/7 as a cross-platform background service.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -30,6 +30,12 @@ export interface RotationTarget {
|
|
|
30
30
|
label: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export interface RotationState {
|
|
34
|
+
generation: number;
|
|
35
|
+
activeId?: string;
|
|
36
|
+
activeLabel?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
export interface AccountRotator {
|
|
34
40
|
/** Whether auto-rotate is switched on. */
|
|
35
41
|
enabled(): boolean;
|
|
@@ -39,9 +45,19 @@ export interface AccountRotator {
|
|
|
39
45
|
activate(id: string): Promise<void>;
|
|
40
46
|
/** Quarantine an account after an account-specific failure. Undefined means active. */
|
|
41
47
|
markFailed(id: string | undefined, reason: string): Promise<void>;
|
|
48
|
+
/** Process/account generation observed when a turn failed. */
|
|
49
|
+
state(): RotationState;
|
|
50
|
+
/** Serialize a complete rotation probe. `changed` means another chat already
|
|
51
|
+
* selected/restarted an account while this caller was waiting. */
|
|
52
|
+
withRotationLock<T>(observed: RotationState, run: (changed: boolean) => Promise<T>): Promise<T>;
|
|
53
|
+
/** Wait for an in-progress rotation probe before re-binding a stale session. */
|
|
54
|
+
waitForIdle(): Promise<void>;
|
|
42
55
|
}
|
|
43
56
|
|
|
44
57
|
export class AccountRotatorImpl implements AccountRotator {
|
|
58
|
+
private generation = 0;
|
|
59
|
+
private rotationTail: Promise<void> = Promise.resolve();
|
|
60
|
+
|
|
45
61
|
constructor(
|
|
46
62
|
private readonly accounts: AccountManager,
|
|
47
63
|
private readonly acp: GrokClient,
|
|
@@ -51,6 +67,42 @@ export class AccountRotatorImpl implements AccountRotator {
|
|
|
51
67
|
return this.accounts.autoRotateEnabled();
|
|
52
68
|
}
|
|
53
69
|
|
|
70
|
+
state(): RotationState {
|
|
71
|
+
const activeId = this.accounts.activeAccountId();
|
|
72
|
+
return {
|
|
73
|
+
generation: this.generation,
|
|
74
|
+
activeId,
|
|
75
|
+
activeLabel: activeId ? this.accounts.get(activeId)?.label : undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async withRotationLock<T>(observed: RotationState, run: (changed: boolean) => Promise<T>): Promise<T> {
|
|
80
|
+
const previous = this.rotationTail;
|
|
81
|
+
let release!: () => void;
|
|
82
|
+
this.rotationTail = new Promise<void>((resolve) => {
|
|
83
|
+
release = resolve;
|
|
84
|
+
});
|
|
85
|
+
await previous;
|
|
86
|
+
try {
|
|
87
|
+
const current = this.state();
|
|
88
|
+
const changed =
|
|
89
|
+
current.generation !== observed.generation || current.activeId !== observed.activeId;
|
|
90
|
+
return await run(changed);
|
|
91
|
+
} finally {
|
|
92
|
+
release();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async waitForIdle(): Promise<void> {
|
|
97
|
+
// Include work queued while we were waiting, not only the first captured
|
|
98
|
+
// promise, so callers never re-bind in the middle of a candidate switch.
|
|
99
|
+
for (;;) {
|
|
100
|
+
const pending = this.rotationTail;
|
|
101
|
+
await pending;
|
|
102
|
+
if (pending === this.rotationTail) return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
54
106
|
async targets(): Promise<RotationTarget[]> {
|
|
55
107
|
const list = this.accounts.list();
|
|
56
108
|
const activeId = this.accounts.activeAccountId();
|
|
@@ -96,12 +148,19 @@ export class AccountRotatorImpl implements AccountRotator {
|
|
|
96
148
|
log.info(`rotating: auth.json now ${meta.label}; starting Grok CLI + re-auth`);
|
|
97
149
|
// start() → connect() → initialize + authenticate(cached_token) against
|
|
98
150
|
// the freshly written auth.json. A live process would keep the old token.
|
|
99
|
-
await this.acp.start();
|
|
151
|
+
await this.acp.start(true);
|
|
152
|
+
this.generation++;
|
|
100
153
|
log.info(`rotating: Grok CLI up on ${meta.label}`);
|
|
101
154
|
} catch (e) {
|
|
102
155
|
// Best-effort recover the agent so the bot stays usable even if the
|
|
103
156
|
// target login was bad.
|
|
104
|
-
await this.acp.
|
|
157
|
+
await this.acp.stopAndWait().catch(() => {});
|
|
158
|
+
await this.acp
|
|
159
|
+
.start(true)
|
|
160
|
+
.then(() => {
|
|
161
|
+
this.generation++;
|
|
162
|
+
})
|
|
163
|
+
.catch((err) => log.warn("post-rotate restart failed:", (err as Error).message));
|
|
105
164
|
throw e;
|
|
106
165
|
}
|
|
107
166
|
}
|
|
@@ -205,9 +205,9 @@ export function registerAccounts(bot: Bot, deps: BotDeps): void {
|
|
|
205
205
|
// Import reuses the live auth.json — just re-bind the agent headlessly.
|
|
206
206
|
try {
|
|
207
207
|
await deps.acp.stopAndWait();
|
|
208
|
-
await deps.acp.start();
|
|
208
|
+
await deps.acp.start(true);
|
|
209
209
|
} catch (e) {
|
|
210
|
-
await deps.acp.start().catch(() => {});
|
|
210
|
+
await deps.acp.start(true).catch(() => {});
|
|
211
211
|
return void rerender(ctx, deps, `\u26A0\uFE0F Imported, but re-bind failed: ${(e as Error).message}`);
|
|
212
212
|
}
|
|
213
213
|
let note = `\u2705 Imported the current login${res.label ? ` (${res.label})` : ""}.`;
|
|
@@ -239,9 +239,9 @@ export function registerAccounts(bot: Bot, deps: BotDeps): void {
|
|
|
239
239
|
try {
|
|
240
240
|
meta = await deps.accounts.switchTo(id);
|
|
241
241
|
// 3) Start agent; it authenticates headlessly with cached_token.
|
|
242
|
-
await deps.acp.start();
|
|
242
|
+
await deps.acp.start(true);
|
|
243
243
|
} catch (e) {
|
|
244
|
-
await deps.acp.start().catch(() => {});
|
|
244
|
+
await deps.acp.start(true).catch(() => {});
|
|
245
245
|
throw e;
|
|
246
246
|
}
|
|
247
247
|
const note = (await deps.usage.isLoggedIn())
|
|
@@ -180,7 +180,7 @@ export class ReauthController {
|
|
|
180
180
|
}
|
|
181
181
|
s.phase = "restarting";
|
|
182
182
|
await this.render(s);
|
|
183
|
-
await this.grok.start();
|
|
183
|
+
await this.grok.start(true);
|
|
184
184
|
agentDown = false;
|
|
185
185
|
s.accountLabel = accountLabel(await this.getAccount?.().catch(() => undefined));
|
|
186
186
|
s.phase = "done";
|
|
@@ -191,7 +191,7 @@ export class ReauthController {
|
|
|
191
191
|
} finally {
|
|
192
192
|
s.abort = undefined;
|
|
193
193
|
this.stopAnim(s);
|
|
194
|
-
if (agentDown) await this.grok.start().catch((e) => log.warn("post-reauth restart failed:", (e as Error).message));
|
|
194
|
+
if (agentDown) await this.grok.start(true).catch((e) => log.warn("post-reauth restart failed:", (e as Error).message));
|
|
195
195
|
await this.render(s);
|
|
196
196
|
}
|
|
197
197
|
}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type GrokClient,
|
|
11
11
|
isAccountRotationError,
|
|
12
12
|
isContextExhaustedError,
|
|
13
|
+
isSessionLifecycleError,
|
|
13
14
|
isTransientError,
|
|
14
15
|
type SessionMetadata,
|
|
15
16
|
} from "../grok/client.js";
|
|
@@ -256,6 +257,7 @@ export class SessionRuntime {
|
|
|
256
257
|
// в”Ђв”Ђ sessions в”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђв”Ђ
|
|
257
258
|
|
|
258
259
|
async startNewSession(cwd: string, projectName?: string): Promise<void> {
|
|
260
|
+
await this.accountRotator?.waitForIdle();
|
|
259
261
|
if (this.busy) await this.cancel();
|
|
260
262
|
await this.bindNewSession(cwd, projectName);
|
|
261
263
|
}
|
|
@@ -430,6 +432,9 @@ export class SessionRuntime {
|
|
|
430
432
|
}
|
|
431
433
|
|
|
432
434
|
private async ensureSession(): Promise<void> {
|
|
435
|
+
// Account rotation restarts the process globally. Do not bind a new chat
|
|
436
|
+
// to a candidate account until the owner has finished probing it.
|
|
437
|
+
await this.accountRotator?.waitForIdle();
|
|
433
438
|
if (this.rebindPending && this.sessionId) {
|
|
434
439
|
// The ACP process is frequently mid-restart the first time we re-bind
|
|
435
440
|
// (auto-restart after a crash, or a fresh bot boot), so a single attempt
|
|
@@ -525,8 +530,10 @@ export class SessionRuntime {
|
|
|
525
530
|
|
|
526
531
|
try {
|
|
527
532
|
const outcome = await this.runPromptWithRetries(content);
|
|
528
|
-
const
|
|
529
|
-
let final =
|
|
533
|
+
const rebound = await this.maybeRecoverAgentSession(input, outcome);
|
|
534
|
+
let final = rebound ?? outcome;
|
|
535
|
+
const recovered = await this.maybeAutoFork(input, final);
|
|
536
|
+
final = recovered ?? final;
|
|
530
537
|
// Last resort: if the turn still failed, rotate through other saved
|
|
531
538
|
// accounts (once) and retry on each until one works.
|
|
532
539
|
const rotated = await this.maybeRotateAccount(input, final);
|
|
@@ -629,6 +636,51 @@ export class SessionRuntime {
|
|
|
629
636
|
return pct !== undefined && pct >= threshold;
|
|
630
637
|
}
|
|
631
638
|
|
|
639
|
+
/** A shared-process restart invalidates this runtime's ACP session binding,
|
|
640
|
+
* but says nothing about account health. Wait for any account probe to
|
|
641
|
+
* settle, re-bind/fork this chat on the selected account, and retry once. */
|
|
642
|
+
private async maybeRecoverAgentSession(
|
|
643
|
+
input: PromptInput,
|
|
644
|
+
outcome: { result?: PromptResult; error?: Error; attempts: number },
|
|
645
|
+
): Promise<{ result?: PromptResult; error?: Error; attempts: number } | undefined> {
|
|
646
|
+
if (
|
|
647
|
+
!outcome.error ||
|
|
648
|
+
!isSessionLifecycleError(outcome.error) ||
|
|
649
|
+
this.cancelled ||
|
|
650
|
+
(this.streamer?.hasOutput ?? false)
|
|
651
|
+
) {
|
|
652
|
+
return undefined;
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
await this.accountRotator?.waitForIdle();
|
|
656
|
+
if (this.cancelled) return outcome;
|
|
657
|
+
const previousId = this.sessionId;
|
|
658
|
+
this.sessionLive = false;
|
|
659
|
+
this.rebindPending = Boolean(previousId);
|
|
660
|
+
await this.ensureSession();
|
|
661
|
+
this.shownToolIds = new Set();
|
|
662
|
+
this.subagentShown = new Map();
|
|
663
|
+
this.streamer?.setFooter(this.hashtags());
|
|
664
|
+
const retryContent = buildContentBlocks(input, {
|
|
665
|
+
reasoning: reasoningDirective(this.reasoning),
|
|
666
|
+
priming: this.primingContext,
|
|
667
|
+
imageOutput: this.cfg.sendAgentImages ? IMAGE_OUTPUT_DIRECTIVE : undefined,
|
|
668
|
+
progress: this.cfg.showProgress ? PROGRESS_DIRECTIVE : undefined,
|
|
669
|
+
});
|
|
670
|
+
this.primingContext = undefined;
|
|
671
|
+
log.info(
|
|
672
|
+
`chat ${this.chatId} recovered lifecycle error on the active account` +
|
|
673
|
+
(previousId && this.sessionId !== previousId
|
|
674
|
+
? ` with fresh session ${this.sessionId?.slice(0, 8)}`
|
|
675
|
+
: " by re-binding its session"),
|
|
676
|
+
);
|
|
677
|
+
return this.runPromptWithRetries(retryContent);
|
|
678
|
+
} catch (error) {
|
|
679
|
+
log.warn(`chat ${this.chatId} session recovery failed: ${(error as Error).message}`);
|
|
680
|
+
return { error: error as Error, attempts: outcome.attempts };
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
632
684
|
/**
|
|
633
685
|
* Auto-fork-on-error recovery. When a turn fails with a *transient* error (or
|
|
634
686
|
* a context-exhaustion error) and nothing was streamed to the user, the
|
|
@@ -700,25 +752,57 @@ export class SessionRuntime {
|
|
|
700
752
|
): Promise<{ result?: PromptResult; error?: Error; attempts: number } | undefined> {
|
|
701
753
|
const rotator = this.accountRotator;
|
|
702
754
|
if (!rotator?.enabled() || !final.error || this.cancelled) return undefined;
|
|
755
|
+
if (isSessionLifecycleError(final.error)) return undefined;
|
|
756
|
+
const originalError = final.error;
|
|
757
|
+
const observed = rotator.state();
|
|
758
|
+
return rotator.withRotationLock(observed, async (changed) => {
|
|
759
|
+
if (this.cancelled) return final;
|
|
760
|
+
if (changed) {
|
|
761
|
+
if (this.streamer?.hasOutput ?? false) return undefined;
|
|
762
|
+
const current = rotator.state();
|
|
763
|
+
const transcript = this.sessionId ? recentTranscript(this.cfg.sessionsDir, this.sessionId) : undefined;
|
|
764
|
+
if (this.foreground) {
|
|
765
|
+
await this.notify(
|
|
766
|
+
`\u{1F504} Reusing ${current.activeLabel ?? "the account selected by another chat"} with a fresh session…`,
|
|
767
|
+
{ replyTo: this.turnReplyTo },
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
log.info(`chat ${this.chatId} reusing account generation ${current.generation} selected by another chat`);
|
|
771
|
+
try {
|
|
772
|
+
await this.bindNewSession(this.cwd, this.projectName);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
return { error: error as Error, attempts: final.attempts };
|
|
775
|
+
}
|
|
776
|
+
this.shownToolIds = new Set();
|
|
777
|
+
this.subagentShown = new Map();
|
|
778
|
+
this.streamer?.setFooter(this.hashtags());
|
|
779
|
+
const content = buildContentBlocks(input, {
|
|
780
|
+
reasoning: reasoningDirective(this.reasoning),
|
|
781
|
+
priming: transcript ? buildPriming(transcript) : undefined,
|
|
782
|
+
imageOutput: this.cfg.sendAgentImages ? IMAGE_OUTPUT_DIRECTIVE : undefined,
|
|
783
|
+
progress: this.cfg.showProgress ? PROGRESS_DIRECTIVE : undefined,
|
|
784
|
+
});
|
|
785
|
+
return this.runPromptWithRetries(content);
|
|
786
|
+
}
|
|
703
787
|
// A quota-exhausted or access-denied response cannot be recovered by retrying
|
|
704
788
|
// this login. Quarantine it before choosing targets, so later rotations do not
|
|
705
789
|
// cycle back to a known-bad account. This intentionally happens before the
|
|
706
790
|
// partial-stream guard: we must not retry/rotate a partial reply, but its
|
|
707
791
|
// account still needs to be skipped during a future rotation.
|
|
708
|
-
if (isAccountRotationError(
|
|
709
|
-
await rotator.markFailed(
|
|
792
|
+
if (isAccountRotationError(originalError)) {
|
|
793
|
+
await rotator.markFailed(observed.activeId, originalError.message);
|
|
710
794
|
}
|
|
711
795
|
if (this.streamer?.hasOutput ?? false) return undefined;
|
|
712
796
|
const targets = await rotator.targets().catch(() => [] as { id: string; label: string }[]);
|
|
713
797
|
if (targets.length === 0) return undefined;
|
|
714
798
|
|
|
715
799
|
const transcript = this.sessionId ? recentTranscript(this.cfg.sessionsDir, this.sessionId) : undefined;
|
|
716
|
-
const errors: string[] = [`\u2022 previous: ${
|
|
800
|
+
const errors: string[] = [`\u2022 previous: ${originalError.message}`];
|
|
717
801
|
let last = final;
|
|
718
802
|
|
|
719
803
|
for (const t of targets) {
|
|
720
804
|
if (this.cancelled) return last;
|
|
721
|
-
const failReason = last.error ??
|
|
805
|
+
const failReason = last.error ?? originalError;
|
|
722
806
|
if (this.foreground) {
|
|
723
807
|
await this.notify(formatAccountSwitchNotice(t.label, failReason), { replyTo: this.turnReplyTo });
|
|
724
808
|
}
|
|
@@ -767,6 +851,7 @@ export class SessionRuntime {
|
|
|
767
851
|
// One full cycle done and still failing — stop with a combined report.
|
|
768
852
|
const combined = new Error(`Tried ${targets.length + 1} account(s), all failed:\n${errors.join("\n")}`);
|
|
769
853
|
return { error: combined, attempts: last.attempts };
|
|
854
|
+
});
|
|
770
855
|
}
|
|
771
856
|
|
|
772
857
|
/**
|
package/src/grok/client.ts
CHANGED
|
@@ -59,6 +59,11 @@ const ACCOUNT_EXHAUSTED_RE =
|
|
|
59
59
|
* saved login may be permitted, while same-account retries cannot help. */
|
|
60
60
|
const ACCOUNT_ACCESS_DENIED_RE =
|
|
61
61
|
/\b403\b|forbidden|access denied/i;
|
|
62
|
+
/** Process/session lifecycle failures are not evidence that the active login
|
|
63
|
+
* is bad. They require a session re-bind on the current process generation,
|
|
64
|
+
* never account rotation. */
|
|
65
|
+
const SESSION_LIFECYCLE_RE =
|
|
66
|
+
/unknown session id|grok agent is restarting|grok agent stdio exited|grok agent (?:is )?not running|agent connection (?:is )?closed|authentication required.{0,80}no auth method id provided/i;
|
|
62
67
|
|
|
63
68
|
export class GrokError extends Error {
|
|
64
69
|
constructor(
|
|
@@ -123,7 +128,14 @@ export function isAccountRotationError(err: Error): boolean {
|
|
|
123
128
|
return false;
|
|
124
129
|
}
|
|
125
130
|
|
|
131
|
+
export function isSessionLifecycleError(err: Error): boolean {
|
|
132
|
+
return SESSION_LIFECYCLE_RE.test(err.message);
|
|
133
|
+
}
|
|
134
|
+
|
|
126
135
|
export function isTransientError(err: Error): boolean {
|
|
136
|
+
// Retrying the same stale session cannot recover a process-generation
|
|
137
|
+
// mismatch. SessionRuntime owns the immediate re-bind + one safe retry.
|
|
138
|
+
if (isSessionLifecycleError(err)) return false;
|
|
127
139
|
// Quota exhaustion and access denial are permanent for this login — rotate,
|
|
128
140
|
// never back off and retry the same credentials.
|
|
129
141
|
if (isAccountRotationError(err)) return false;
|
|
@@ -265,9 +277,10 @@ export class GrokClient extends EventEmitter {
|
|
|
265
277
|
this.availableModels = KNOWN_MODELS.map((m) => ({ modelId: m.modelId, name: m.name, description: m.description }));
|
|
266
278
|
}
|
|
267
279
|
|
|
268
|
-
async start(): Promise<void> {
|
|
280
|
+
async start(notifyRestarted = false): Promise<void> {
|
|
269
281
|
this.stopped = false;
|
|
270
282
|
await this.connect();
|
|
283
|
+
if (notifyRestarted) this.emit("restarted");
|
|
271
284
|
}
|
|
272
285
|
|
|
273
286
|
private async connect(): Promise<void> {
|
|
@@ -529,9 +542,7 @@ export class GrokClient extends EventEmitter {
|
|
|
529
542
|
this.stopped = true;
|
|
530
543
|
this.restartAttempts = 0;
|
|
531
544
|
await this.killCurrent();
|
|
532
|
-
this.
|
|
533
|
-
await this.connect();
|
|
534
|
-
this.emit("restarted");
|
|
545
|
+
await this.start(true);
|
|
535
546
|
}
|
|
536
547
|
|
|
537
548
|
private killCurrent(): Promise<void> {
|