ctb 1.2.1 → 1.3.0
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/README.md +8 -0
- package/docs/plans/2026-02-01-performance-reliability-improvements.md +1038 -0
- package/package.json +1 -1
- package/src/__tests__/errors.test.ts +60 -0
- package/src/__tests__/events.test.ts +43 -0
- package/src/__tests__/session.test.ts +73 -0
- package/src/__tests__/telegram-api.test.ts +80 -0
- package/src/errors.ts +58 -0
- package/src/events.ts +57 -0
- package/src/handlers/streaming.ts +23 -27
- package/src/handlers/text.ts +2 -1
- package/src/index.ts +34 -16
- package/src/session.ts +67 -1
- package/src/telegram-api.ts +195 -0
- package/src/types.ts +34 -33
- package/src/utils.ts +4 -21
|
@@ -0,0 +1,1038 @@
|
|
|
1
|
+
# Performance, Reliability, Architecture & UX Improvements
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Implement high-impact performance optimizations, reliability improvements, architecture cleanup, and UX enhancements for the Claude Telegram Bot.
|
|
6
|
+
|
|
7
|
+
**Architecture:** Debounced session persistence, centralized error handling with retry logic, elimination of circular dependencies via event emitter pattern, and progress feedback for long operations.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Bun, TypeScript, grammY, Claude Agent SDK
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Task 1: Session Write Debouncing (Performance)
|
|
14
|
+
|
|
15
|
+
**Files:**
|
|
16
|
+
|
|
17
|
+
- Modify: `src/session.ts:635-649`
|
|
18
|
+
- Test: `src/__tests__/session.test.ts` (create)
|
|
19
|
+
|
|
20
|
+
**Problem:** `saveSession()` writes to disk synchronously on every `session_id` capture. With frequent operations, this causes unnecessary I/O.
|
|
21
|
+
|
|
22
|
+
**Step 1: Write the failing test**
|
|
23
|
+
|
|
24
|
+
Create `src/__tests__/session.test.ts`:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
/**
|
|
28
|
+
* Unit tests for session module.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
32
|
+
|
|
33
|
+
describe("Session debouncing", () => {
|
|
34
|
+
test("multiple saveSession calls within debounce window only write once", async () => {
|
|
35
|
+
// We'll test this by checking the debounce behavior
|
|
36
|
+
let writeCount = 0;
|
|
37
|
+
const originalWrite = Bun.write;
|
|
38
|
+
|
|
39
|
+
// Mock Bun.write to count calls
|
|
40
|
+
(Bun as any).write = async (...args: any[]) => {
|
|
41
|
+
if (String(args[0]).includes("session.json")) {
|
|
42
|
+
writeCount++;
|
|
43
|
+
}
|
|
44
|
+
return originalWrite.apply(Bun, args as any);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Import fresh session module
|
|
48
|
+
const { ClaudeSession } = await import("../session");
|
|
49
|
+
const testSession = new ClaudeSession();
|
|
50
|
+
|
|
51
|
+
// Trigger multiple saves rapidly
|
|
52
|
+
(testSession as any).sessionId = "test-session-1";
|
|
53
|
+
(testSession as any).saveSession();
|
|
54
|
+
(testSession as any).saveSession();
|
|
55
|
+
(testSession as any).saveSession();
|
|
56
|
+
|
|
57
|
+
// Should not have written yet (debounced)
|
|
58
|
+
expect(writeCount).toBe(0);
|
|
59
|
+
|
|
60
|
+
// Wait for debounce to complete
|
|
61
|
+
await Bun.sleep(600);
|
|
62
|
+
|
|
63
|
+
// Should have written exactly once
|
|
64
|
+
expect(writeCount).toBe(1);
|
|
65
|
+
|
|
66
|
+
// Restore
|
|
67
|
+
(Bun as any).write = originalWrite;
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Step 2: Run test to verify it fails**
|
|
73
|
+
|
|
74
|
+
Run: `bun test src/__tests__/session.test.ts`
|
|
75
|
+
Expected: FAIL - current implementation writes immediately
|
|
76
|
+
|
|
77
|
+
**Step 3: Implement debounced session saving**
|
|
78
|
+
|
|
79
|
+
In `src/session.ts`, add debounce mechanism:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// Add after line 105 (private _isProcessing declaration)
|
|
83
|
+
private _saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
84
|
+
private _pendingSave = false;
|
|
85
|
+
private static SAVE_DEBOUNCE_MS = 500;
|
|
86
|
+
|
|
87
|
+
// Replace saveSession method (lines 635-649)
|
|
88
|
+
/**
|
|
89
|
+
* Save session to disk for resume after restart.
|
|
90
|
+
* Debounced to avoid frequent disk writes.
|
|
91
|
+
*/
|
|
92
|
+
private saveSession(): void {
|
|
93
|
+
if (!this.sessionId) return;
|
|
94
|
+
|
|
95
|
+
this._pendingSave = true;
|
|
96
|
+
|
|
97
|
+
// Clear existing timeout
|
|
98
|
+
if (this._saveTimeout) {
|
|
99
|
+
clearTimeout(this._saveTimeout);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Debounce writes
|
|
103
|
+
this._saveTimeout = setTimeout(() => {
|
|
104
|
+
if (!this._pendingSave || !this.sessionId) return;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const data: SessionData = {
|
|
108
|
+
session_id: this.sessionId,
|
|
109
|
+
saved_at: new Date().toISOString(),
|
|
110
|
+
working_dir: this._workingDir,
|
|
111
|
+
};
|
|
112
|
+
Bun.write(SESSION_FILE, JSON.stringify(data));
|
|
113
|
+
console.log(`Session saved to ${SESSION_FILE}`);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.warn(`Failed to save session: ${error}`);
|
|
116
|
+
}
|
|
117
|
+
this._pendingSave = false;
|
|
118
|
+
this._saveTimeout = null;
|
|
119
|
+
}, ClaudeSession.SAVE_DEBOUNCE_MS);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Force immediate session save (for shutdown).
|
|
124
|
+
*/
|
|
125
|
+
flushSession(): void {
|
|
126
|
+
if (this._saveTimeout) {
|
|
127
|
+
clearTimeout(this._saveTimeout);
|
|
128
|
+
this._saveTimeout = null;
|
|
129
|
+
}
|
|
130
|
+
if (this._pendingSave && this.sessionId) {
|
|
131
|
+
try {
|
|
132
|
+
const data: SessionData = {
|
|
133
|
+
session_id: this.sessionId,
|
|
134
|
+
saved_at: new Date().toISOString(),
|
|
135
|
+
working_dir: this._workingDir,
|
|
136
|
+
};
|
|
137
|
+
// Use sync write for shutdown
|
|
138
|
+
const fs = require("fs");
|
|
139
|
+
fs.writeFileSync(SESSION_FILE, JSON.stringify(data));
|
|
140
|
+
console.log(`Session flushed to ${SESSION_FILE}`);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.warn(`Failed to flush session: ${error}`);
|
|
143
|
+
}
|
|
144
|
+
this._pendingSave = false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Step 4: Run test to verify it passes**
|
|
150
|
+
|
|
151
|
+
Run: `bun test src/__tests__/session.test.ts`
|
|
152
|
+
Expected: PASS
|
|
153
|
+
|
|
154
|
+
**Step 5: Commit**
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
git add src/session.ts src/__tests__/session.test.ts
|
|
158
|
+
git commit -m "perf: add debounced session writes to reduce disk I/O"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Task 2: Telegram API Error Handling with Retry (Reliability)
|
|
164
|
+
|
|
165
|
+
**Files:**
|
|
166
|
+
|
|
167
|
+
- Create: `src/telegram-api.ts`
|
|
168
|
+
- Modify: `src/handlers/streaming.ts`
|
|
169
|
+
- Test: `src/__tests__/telegram-api.test.ts`
|
|
170
|
+
|
|
171
|
+
**Problem:** Telegram API calls can fail transiently (429 rate limits, network issues). Currently errors are logged but not retried.
|
|
172
|
+
|
|
173
|
+
**Step 1: Write the failing test**
|
|
174
|
+
|
|
175
|
+
Create `src/__tests__/telegram-api.test.ts`:
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
/**
|
|
179
|
+
* Unit tests for Telegram API utilities.
|
|
180
|
+
*/
|
|
181
|
+
|
|
182
|
+
import { describe, expect, test } from "bun:test";
|
|
183
|
+
import { withRetry, TelegramApiError } from "../telegram-api";
|
|
184
|
+
|
|
185
|
+
describe("withRetry", () => {
|
|
186
|
+
test("succeeds on first attempt", async () => {
|
|
187
|
+
let attempts = 0;
|
|
188
|
+
const result = await withRetry(async () => {
|
|
189
|
+
attempts++;
|
|
190
|
+
return "success";
|
|
191
|
+
});
|
|
192
|
+
expect(result).toBe("success");
|
|
193
|
+
expect(attempts).toBe(1);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("retries on transient failure then succeeds", async () => {
|
|
197
|
+
let attempts = 0;
|
|
198
|
+
const result = await withRetry(
|
|
199
|
+
async () => {
|
|
200
|
+
attempts++;
|
|
201
|
+
if (attempts < 3) {
|
|
202
|
+
throw new Error("Too Many Requests: retry after 1");
|
|
203
|
+
}
|
|
204
|
+
return "success";
|
|
205
|
+
},
|
|
206
|
+
{ maxRetries: 3, baseDelay: 10 },
|
|
207
|
+
);
|
|
208
|
+
expect(result).toBe("success");
|
|
209
|
+
expect(attempts).toBe(3);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("throws after max retries", async () => {
|
|
213
|
+
let attempts = 0;
|
|
214
|
+
await expect(
|
|
215
|
+
withRetry(
|
|
216
|
+
async () => {
|
|
217
|
+
attempts++;
|
|
218
|
+
throw new Error("Too Many Requests: retry after 1");
|
|
219
|
+
},
|
|
220
|
+
{ maxRetries: 2, baseDelay: 10 },
|
|
221
|
+
),
|
|
222
|
+
).rejects.toThrow();
|
|
223
|
+
expect(attempts).toBe(2);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("does not retry non-transient errors", async () => {
|
|
227
|
+
let attempts = 0;
|
|
228
|
+
await expect(
|
|
229
|
+
withRetry(
|
|
230
|
+
async () => {
|
|
231
|
+
attempts++;
|
|
232
|
+
throw new Error("Bad Request: message not found");
|
|
233
|
+
},
|
|
234
|
+
{ maxRetries: 3, baseDelay: 10 },
|
|
235
|
+
),
|
|
236
|
+
).rejects.toThrow("Bad Request");
|
|
237
|
+
expect(attempts).toBe(1);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("TelegramApiError", () => {
|
|
242
|
+
test("isTransient returns true for rate limit errors", () => {
|
|
243
|
+
const error = new TelegramApiError("Too Many Requests: retry after 5", 429);
|
|
244
|
+
expect(error.isTransient).toBe(true);
|
|
245
|
+
expect(error.retryAfter).toBe(5);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("isTransient returns true for network errors", () => {
|
|
249
|
+
const error = new TelegramApiError("ETIMEDOUT", 0);
|
|
250
|
+
expect(error.isTransient).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("isTransient returns false for bad request", () => {
|
|
254
|
+
const error = new TelegramApiError("Bad Request: message not found", 400);
|
|
255
|
+
expect(error.isTransient).toBe(false);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Step 2: Run test to verify it fails**
|
|
261
|
+
|
|
262
|
+
Run: `bun test src/__tests__/telegram-api.test.ts`
|
|
263
|
+
Expected: FAIL - module doesn't exist
|
|
264
|
+
|
|
265
|
+
**Step 3: Implement Telegram API utilities**
|
|
266
|
+
|
|
267
|
+
Create `src/telegram-api.ts`:
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
/**
|
|
271
|
+
* Telegram API utilities with retry logic.
|
|
272
|
+
*/
|
|
273
|
+
|
|
274
|
+
export interface RetryOptions {
|
|
275
|
+
maxRetries?: number;
|
|
276
|
+
baseDelay?: number;
|
|
277
|
+
maxDelay?: number;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const DEFAULT_OPTIONS: Required<RetryOptions> = {
|
|
281
|
+
maxRetries: 3,
|
|
282
|
+
baseDelay: 1000,
|
|
283
|
+
maxDelay: 10000,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Telegram API error with transient detection.
|
|
288
|
+
*/
|
|
289
|
+
export class TelegramApiError extends Error {
|
|
290
|
+
constructor(
|
|
291
|
+
message: string,
|
|
292
|
+
public readonly statusCode: number,
|
|
293
|
+
public readonly retryAfter?: number,
|
|
294
|
+
) {
|
|
295
|
+
super(message);
|
|
296
|
+
this.name = "TelegramApiError";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Whether this error is transient and should be retried.
|
|
301
|
+
*/
|
|
302
|
+
get isTransient(): boolean {
|
|
303
|
+
// Rate limited
|
|
304
|
+
if (this.statusCode === 429) return true;
|
|
305
|
+
|
|
306
|
+
// Network errors
|
|
307
|
+
const networkErrors = ["ETIMEDOUT", "ECONNRESET", "ENOTFOUND", "EAI_AGAIN"];
|
|
308
|
+
if (networkErrors.some((e) => this.message.includes(e))) return true;
|
|
309
|
+
|
|
310
|
+
// Telegram "Too Many Requests"
|
|
311
|
+
if (this.message.includes("Too Many Requests")) return true;
|
|
312
|
+
|
|
313
|
+
// Server errors (5xx)
|
|
314
|
+
if (this.statusCode >= 500 && this.statusCode < 600) return true;
|
|
315
|
+
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Parse retry-after from Telegram error message.
|
|
322
|
+
*/
|
|
323
|
+
function parseRetryAfter(error: Error): number | undefined {
|
|
324
|
+
const match = error.message.match(/retry after (\d+)/i);
|
|
325
|
+
return match ? parseInt(match[1], 10) : undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Check if an error is transient.
|
|
330
|
+
*/
|
|
331
|
+
function isTransientError(error: Error): boolean {
|
|
332
|
+
const msg = error.message.toLowerCase();
|
|
333
|
+
|
|
334
|
+
// Rate limiting
|
|
335
|
+
if (msg.includes("too many requests") || msg.includes("retry after"))
|
|
336
|
+
return true;
|
|
337
|
+
|
|
338
|
+
// Network errors
|
|
339
|
+
if (msg.includes("etimedout") || msg.includes("econnreset")) return true;
|
|
340
|
+
if (msg.includes("enotfound") || msg.includes("eai_again")) return true;
|
|
341
|
+
|
|
342
|
+
// Telegram flood control
|
|
343
|
+
if (msg.includes("flood")) return true;
|
|
344
|
+
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Execute a function with exponential backoff retry.
|
|
350
|
+
*/
|
|
351
|
+
export async function withRetry<T>(
|
|
352
|
+
fn: () => Promise<T>,
|
|
353
|
+
options?: RetryOptions,
|
|
354
|
+
): Promise<T> {
|
|
355
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
356
|
+
let lastError: Error | undefined;
|
|
357
|
+
|
|
358
|
+
for (let attempt = 1; attempt <= opts.maxRetries; attempt++) {
|
|
359
|
+
try {
|
|
360
|
+
return await fn();
|
|
361
|
+
} catch (error) {
|
|
362
|
+
lastError = error as Error;
|
|
363
|
+
|
|
364
|
+
// Don't retry non-transient errors
|
|
365
|
+
if (!isTransientError(lastError)) {
|
|
366
|
+
throw lastError;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Last attempt - throw
|
|
370
|
+
if (attempt === opts.maxRetries) {
|
|
371
|
+
throw lastError;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Calculate delay with exponential backoff
|
|
375
|
+
const retryAfter = parseRetryAfter(lastError);
|
|
376
|
+
const delay = retryAfter
|
|
377
|
+
? retryAfter * 1000
|
|
378
|
+
: Math.min(opts.baseDelay * Math.pow(2, attempt - 1), opts.maxDelay);
|
|
379
|
+
|
|
380
|
+
console.debug(
|
|
381
|
+
`Retry ${attempt}/${opts.maxRetries} after ${delay}ms: ${lastError.message}`,
|
|
382
|
+
);
|
|
383
|
+
await Bun.sleep(delay);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
throw lastError;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Safe Telegram API call wrapper - logs errors but doesn't throw for non-critical operations.
|
|
392
|
+
*/
|
|
393
|
+
export async function safeTelegramCall<T>(
|
|
394
|
+
operation: string,
|
|
395
|
+
fn: () => Promise<T>,
|
|
396
|
+
fallback?: T,
|
|
397
|
+
): Promise<T | undefined> {
|
|
398
|
+
try {
|
|
399
|
+
return await withRetry(fn);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
console.warn(`Telegram ${operation} failed:`, error);
|
|
402
|
+
return fallback;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Step 4: Run test to verify it passes**
|
|
408
|
+
|
|
409
|
+
Run: `bun test src/__tests__/telegram-api.test.ts`
|
|
410
|
+
Expected: PASS
|
|
411
|
+
|
|
412
|
+
**Step 5: Update streaming.ts to use retry logic**
|
|
413
|
+
|
|
414
|
+
In `src/handlers/streaming.ts`, import and use:
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
// Add import at top
|
|
418
|
+
import { safeTelegramCall } from "../telegram-api";
|
|
419
|
+
|
|
420
|
+
// Update message edit calls (around line 149) to use safeTelegramCall:
|
|
421
|
+
// Replace direct ctx.api.editMessageText calls with:
|
|
422
|
+
await safeTelegramCall("editMessage", () =>
|
|
423
|
+
ctx.api.editMessageText(msg.chat.id, msg.message_id, formatted, {
|
|
424
|
+
parse_mode: "HTML",
|
|
425
|
+
}),
|
|
426
|
+
);
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**Step 6: Run all tests**
|
|
430
|
+
|
|
431
|
+
Run: `bun test`
|
|
432
|
+
Expected: All PASS
|
|
433
|
+
|
|
434
|
+
**Step 7: Commit**
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
git add src/telegram-api.ts src/__tests__/telegram-api.test.ts src/handlers/streaming.ts
|
|
438
|
+
git commit -m "feat: add Telegram API retry logic for transient errors"
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## Task 3: Eliminate Circular Dependency (Architecture)
|
|
444
|
+
|
|
445
|
+
**Files:**
|
|
446
|
+
|
|
447
|
+
- Create: `src/events.ts`
|
|
448
|
+
- Modify: `src/utils.ts`
|
|
449
|
+
- Modify: `src/session.ts`
|
|
450
|
+
- Test: `src/__tests__/events.test.ts`
|
|
451
|
+
|
|
452
|
+
**Problem:** `utils.ts` lazily imports `session.ts` to check `isRunning` for the `!` interrupt feature. This creates a circular dependency.
|
|
453
|
+
|
|
454
|
+
**Solution:** Use an event emitter pattern. Session emits events, utils subscribes.
|
|
455
|
+
|
|
456
|
+
**Step 1: Write the failing test**
|
|
457
|
+
|
|
458
|
+
Create `src/__tests__/events.test.ts`:
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
/**
|
|
462
|
+
* Unit tests for event emitter module.
|
|
463
|
+
*/
|
|
464
|
+
|
|
465
|
+
import { describe, expect, test } from "bun:test";
|
|
466
|
+
import { botEvents } from "../events";
|
|
467
|
+
|
|
468
|
+
describe("BotEvents", () => {
|
|
469
|
+
test("emits and receives events", () => {
|
|
470
|
+
let received = false;
|
|
471
|
+
const unsubscribe = botEvents.on("sessionRunning", (running) => {
|
|
472
|
+
received = running;
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
botEvents.emit("sessionRunning", true);
|
|
476
|
+
expect(received).toBe(true);
|
|
477
|
+
|
|
478
|
+
unsubscribe();
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("unsubscribe stops receiving events", () => {
|
|
482
|
+
let count = 0;
|
|
483
|
+
const unsubscribe = botEvents.on("sessionRunning", () => {
|
|
484
|
+
count++;
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
botEvents.emit("sessionRunning", true);
|
|
488
|
+
expect(count).toBe(1);
|
|
489
|
+
|
|
490
|
+
unsubscribe();
|
|
491
|
+
|
|
492
|
+
botEvents.emit("sessionRunning", true);
|
|
493
|
+
expect(count).toBe(1); // Still 1, not 2
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("getSessionState returns current state", () => {
|
|
497
|
+
botEvents.emit("sessionRunning", false);
|
|
498
|
+
expect(botEvents.getSessionState()).toBe(false);
|
|
499
|
+
|
|
500
|
+
botEvents.emit("sessionRunning", true);
|
|
501
|
+
expect(botEvents.getSessionState()).toBe(true);
|
|
502
|
+
});
|
|
503
|
+
});
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
**Step 2: Run test to verify it fails**
|
|
507
|
+
|
|
508
|
+
Run: `bun test src/__tests__/events.test.ts`
|
|
509
|
+
Expected: FAIL - module doesn't exist
|
|
510
|
+
|
|
511
|
+
**Step 3: Implement event emitter**
|
|
512
|
+
|
|
513
|
+
Create `src/events.ts`:
|
|
514
|
+
|
|
515
|
+
```typescript
|
|
516
|
+
/**
|
|
517
|
+
* Lightweight event emitter for decoupling modules.
|
|
518
|
+
* Eliminates circular dependencies between session and utils.
|
|
519
|
+
*/
|
|
520
|
+
|
|
521
|
+
type EventCallback<T> = (data: T) => void;
|
|
522
|
+
|
|
523
|
+
interface BotEventsMap {
|
|
524
|
+
sessionRunning: boolean;
|
|
525
|
+
stopRequested: void;
|
|
526
|
+
interruptRequested: void;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
class BotEventEmitter {
|
|
530
|
+
private listeners = new Map<keyof BotEventsMap, Set<EventCallback<any>>>();
|
|
531
|
+
private sessionRunning = false;
|
|
532
|
+
|
|
533
|
+
on<K extends keyof BotEventsMap>(
|
|
534
|
+
event: K,
|
|
535
|
+
callback: EventCallback<BotEventsMap[K]>,
|
|
536
|
+
): () => void {
|
|
537
|
+
if (!this.listeners.has(event)) {
|
|
538
|
+
this.listeners.set(event, new Set());
|
|
539
|
+
}
|
|
540
|
+
this.listeners.get(event)!.add(callback);
|
|
541
|
+
|
|
542
|
+
// Return unsubscribe function
|
|
543
|
+
return () => {
|
|
544
|
+
this.listeners.get(event)?.delete(callback);
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
emit<K extends keyof BotEventsMap>(event: K, data: BotEventsMap[K]): void {
|
|
549
|
+
// Track session state
|
|
550
|
+
if (event === "sessionRunning") {
|
|
551
|
+
this.sessionRunning = data as boolean;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const callbacks = this.listeners.get(event);
|
|
555
|
+
if (callbacks) {
|
|
556
|
+
for (const callback of callbacks) {
|
|
557
|
+
try {
|
|
558
|
+
callback(data);
|
|
559
|
+
} catch (error) {
|
|
560
|
+
console.error(`Event handler error for ${event}:`, error);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Get current session running state without importing session module.
|
|
568
|
+
*/
|
|
569
|
+
getSessionState(): boolean {
|
|
570
|
+
return this.sessionRunning;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
export const botEvents = new BotEventEmitter();
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Step 4: Run test to verify it passes**
|
|
578
|
+
|
|
579
|
+
Run: `bun test src/__tests__/events.test.ts`
|
|
580
|
+
Expected: PASS
|
|
581
|
+
|
|
582
|
+
**Step 5: Update session.ts to emit events**
|
|
583
|
+
|
|
584
|
+
In `src/session.ts`, add event emission:
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
// Add import at top
|
|
588
|
+
import { botEvents } from "./events";
|
|
589
|
+
|
|
590
|
+
// In sendMessageStreaming, after setting isQueryRunning = true (line 295):
|
|
591
|
+
botEvents.emit("sessionRunning", true);
|
|
592
|
+
|
|
593
|
+
// In sendMessageStreaming finally block (after line 514):
|
|
594
|
+
botEvents.emit("sessionRunning", false);
|
|
595
|
+
|
|
596
|
+
// In stop() method, after setting stopRequested = true:
|
|
597
|
+
botEvents.emit("stopRequested", undefined);
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
**Step 6: Update utils.ts to use events instead of lazy import**
|
|
601
|
+
|
|
602
|
+
Replace the lazy import section in `src/utils.ts` (lines 214-246):
|
|
603
|
+
|
|
604
|
+
```typescript
|
|
605
|
+
// Remove the lazy import section and replace with:
|
|
606
|
+
import { botEvents } from "./events";
|
|
607
|
+
|
|
608
|
+
export async function checkInterrupt(text: string): Promise<string> {
|
|
609
|
+
if (!text || !text.startsWith("!")) {
|
|
610
|
+
return text;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const strippedText = text.slice(1).trimStart();
|
|
614
|
+
|
|
615
|
+
// Check session state via events (no circular dependency)
|
|
616
|
+
if (botEvents.getSessionState()) {
|
|
617
|
+
console.log("! prefix - requesting interrupt");
|
|
618
|
+
botEvents.emit("interruptRequested", undefined);
|
|
619
|
+
await Bun.sleep(100);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return strippedText;
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
**Step 7: Update session.ts to listen for interrupt events**
|
|
627
|
+
|
|
628
|
+
In `src/session.ts`, in the constructor or initialization:
|
|
629
|
+
|
|
630
|
+
```typescript
|
|
631
|
+
// Add in ClaudeSession class, after property declarations:
|
|
632
|
+
constructor() {
|
|
633
|
+
// Listen for interrupt requests from utils
|
|
634
|
+
botEvents.on("interruptRequested", () => {
|
|
635
|
+
if (this.isRunning) {
|
|
636
|
+
this.markInterrupt();
|
|
637
|
+
this.stop();
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
**Step 8: Run all tests**
|
|
644
|
+
|
|
645
|
+
Run: `bun test`
|
|
646
|
+
Expected: All PASS
|
|
647
|
+
|
|
648
|
+
**Step 9: Commit**
|
|
649
|
+
|
|
650
|
+
```bash
|
|
651
|
+
git add src/events.ts src/__tests__/events.test.ts src/utils.ts src/session.ts
|
|
652
|
+
git commit -m "refactor: eliminate circular dependency with event emitter pattern"
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
## Task 4: Graceful Shutdown with Timeout (Reliability)
|
|
658
|
+
|
|
659
|
+
**Files:**
|
|
660
|
+
|
|
661
|
+
- Modify: `src/index.ts`
|
|
662
|
+
- Modify: `src/session.ts`
|
|
663
|
+
|
|
664
|
+
**Problem:** On shutdown, pending operations may not complete cleanly.
|
|
665
|
+
|
|
666
|
+
**Step 1: Implement graceful shutdown in index.ts**
|
|
667
|
+
|
|
668
|
+
Add shutdown handler at the end of `src/index.ts`:
|
|
669
|
+
|
|
670
|
+
```typescript
|
|
671
|
+
// Graceful shutdown
|
|
672
|
+
const SHUTDOWN_TIMEOUT_MS = 5000;
|
|
673
|
+
|
|
674
|
+
async function gracefulShutdown(signal: string): Promise<void> {
|
|
675
|
+
console.log(`\n${signal} received - initiating graceful shutdown...`);
|
|
676
|
+
|
|
677
|
+
// Set a hard timeout
|
|
678
|
+
const forceExit = setTimeout(() => {
|
|
679
|
+
console.error("Shutdown timeout - forcing exit");
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}, SHUTDOWN_TIMEOUT_MS);
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
// Stop the bot (stops polling)
|
|
685
|
+
await bot.stop();
|
|
686
|
+
console.log("Bot stopped");
|
|
687
|
+
|
|
688
|
+
// Flush session data
|
|
689
|
+
session.flushSession();
|
|
690
|
+
console.log("Session flushed");
|
|
691
|
+
|
|
692
|
+
// Clear the timeout and exit cleanly
|
|
693
|
+
clearTimeout(forceExit);
|
|
694
|
+
console.log("Shutdown complete");
|
|
695
|
+
process.exit(0);
|
|
696
|
+
} catch (error) {
|
|
697
|
+
console.error("Error during shutdown:", error);
|
|
698
|
+
clearTimeout(forceExit);
|
|
699
|
+
process.exit(1);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
704
|
+
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
**Step 2: Commit**
|
|
708
|
+
|
|
709
|
+
```bash
|
|
710
|
+
git add src/index.ts
|
|
711
|
+
git commit -m "feat: add graceful shutdown with timeout"
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
---
|
|
715
|
+
|
|
716
|
+
## Task 5: Progress Feedback for Long Operations (UX)
|
|
717
|
+
|
|
718
|
+
**Files:**
|
|
719
|
+
|
|
720
|
+
- Modify: `src/handlers/streaming.ts`
|
|
721
|
+
- Modify: `src/types.ts`
|
|
722
|
+
|
|
723
|
+
**Problem:** During long tool executions, users see no feedback.
|
|
724
|
+
|
|
725
|
+
**Step 1: Add elapsed time to tool status**
|
|
726
|
+
|
|
727
|
+
In `src/handlers/streaming.ts`, update the tool status display:
|
|
728
|
+
|
|
729
|
+
```typescript
|
|
730
|
+
// Add a timestamp tracker in StreamingState class:
|
|
731
|
+
export class StreamingState {
|
|
732
|
+
textMessages = new Map<number, Message>();
|
|
733
|
+
toolMessages: Message[] = [];
|
|
734
|
+
lastEditTimes = new Map<number, number>();
|
|
735
|
+
lastContent = new Map<number, string>();
|
|
736
|
+
toolStartTime: number | null = null; // Add this
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// In createStatusCallback, update tool handling:
|
|
740
|
+
} else if (statusType === "tool") {
|
|
741
|
+
state.toolStartTime = Date.now();
|
|
742
|
+
const toolMsg = await ctx.reply(content, { parse_mode: "HTML" });
|
|
743
|
+
state.toolMessages.push(toolMsg);
|
|
744
|
+
}
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
**Step 2: Commit**
|
|
748
|
+
|
|
749
|
+
```bash
|
|
750
|
+
git add src/handlers/streaming.ts src/types.ts
|
|
751
|
+
git commit -m "feat: track tool execution timing for progress feedback"
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
---
|
|
755
|
+
|
|
756
|
+
## Task 6: Friendly Error Messages (UX)
|
|
757
|
+
|
|
758
|
+
**Files:**
|
|
759
|
+
|
|
760
|
+
- Create: `src/errors.ts`
|
|
761
|
+
- Modify: `src/handlers/text.ts`
|
|
762
|
+
- Test: `src/__tests__/errors.test.ts`
|
|
763
|
+
|
|
764
|
+
**Step 1: Write the failing test**
|
|
765
|
+
|
|
766
|
+
Create `src/__tests__/errors.test.ts`:
|
|
767
|
+
|
|
768
|
+
```typescript
|
|
769
|
+
/**
|
|
770
|
+
* Unit tests for error formatting.
|
|
771
|
+
*/
|
|
772
|
+
|
|
773
|
+
import { describe, expect, test } from "bun:test";
|
|
774
|
+
import { formatUserError } from "../errors";
|
|
775
|
+
|
|
776
|
+
describe("formatUserError", () => {
|
|
777
|
+
test("formats timeout error", () => {
|
|
778
|
+
const msg = formatUserError(new Error("Query timeout (180s > 180s limit)"));
|
|
779
|
+
expect(msg).toContain("took too long");
|
|
780
|
+
expect(msg).not.toContain("timeout");
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
test("formats rate limit error", () => {
|
|
784
|
+
const msg = formatUserError(new Error("Too Many Requests: retry after 5"));
|
|
785
|
+
expect(msg).toContain("busy");
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
test("formats network error", () => {
|
|
789
|
+
const msg = formatUserError(new Error("ETIMEDOUT"));
|
|
790
|
+
expect(msg).toContain("connection");
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
test("formats generic error with truncation", () => {
|
|
794
|
+
const longError = "A".repeat(300);
|
|
795
|
+
const msg = formatUserError(new Error(longError));
|
|
796
|
+
expect(msg.length).toBeLessThan(250);
|
|
797
|
+
});
|
|
798
|
+
});
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
**Step 2: Run test to verify it fails**
|
|
802
|
+
|
|
803
|
+
Run: `bun test src/__tests__/errors.test.ts`
|
|
804
|
+
Expected: FAIL - module doesn't exist
|
|
805
|
+
|
|
806
|
+
**Step 3: Implement error formatting**
|
|
807
|
+
|
|
808
|
+
Create `src/errors.ts`:
|
|
809
|
+
|
|
810
|
+
```typescript
|
|
811
|
+
/**
|
|
812
|
+
* User-friendly error message formatting.
|
|
813
|
+
*/
|
|
814
|
+
|
|
815
|
+
interface ErrorPattern {
|
|
816
|
+
pattern: RegExp;
|
|
817
|
+
message: string;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const ERROR_PATTERNS: ErrorPattern[] = [
|
|
821
|
+
{
|
|
822
|
+
pattern: /timeout/i,
|
|
823
|
+
message:
|
|
824
|
+
"⏱️ The operation took too long. Try a simpler request or break it into smaller steps.",
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
pattern: /too many requests|rate limit|retry after/i,
|
|
828
|
+
message: "⏳ Claude is busy right now. Please wait a moment and try again.",
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
pattern: /etimedout|econnreset|enotfound/i,
|
|
832
|
+
message: "🌐 Connection issue. Please check your network and try again.",
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
pattern: /cancelled|aborted/i,
|
|
836
|
+
message: "🛑 Request was cancelled.",
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
pattern: /unsafe command|blocked/i,
|
|
840
|
+
message: "🚫 That operation isn't allowed for safety reasons.",
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
pattern: /file access|outside allowed paths/i,
|
|
844
|
+
message: "📁 Claude can't access that file location.",
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
pattern: /authentication|unauthorized|401/i,
|
|
848
|
+
message: "🔑 Authentication issue. Please check your credentials.",
|
|
849
|
+
},
|
|
850
|
+
];
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Convert technical errors to user-friendly messages.
|
|
854
|
+
*/
|
|
855
|
+
export function formatUserError(error: Error): string {
|
|
856
|
+
const errorStr = error.message || String(error);
|
|
857
|
+
|
|
858
|
+
for (const { pattern, message } of ERROR_PATTERNS) {
|
|
859
|
+
if (pattern.test(errorStr)) {
|
|
860
|
+
return message;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Generic fallback with truncation
|
|
865
|
+
const truncated =
|
|
866
|
+
errorStr.length > 200 ? errorStr.slice(0, 200) + "..." : errorStr;
|
|
867
|
+
return `❌ Error: ${truncated}`;
|
|
868
|
+
}
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
**Step 4: Run test to verify it passes**
|
|
872
|
+
|
|
873
|
+
Run: `bun test src/__tests__/errors.test.ts`
|
|
874
|
+
Expected: PASS
|
|
875
|
+
|
|
876
|
+
**Step 5: Update text.ts to use friendly errors**
|
|
877
|
+
|
|
878
|
+
In `src/handlers/text.ts`, import and use:
|
|
879
|
+
|
|
880
|
+
```typescript
|
|
881
|
+
import { formatUserError } from "../errors";
|
|
882
|
+
|
|
883
|
+
// In catch blocks, replace raw error messages:
|
|
884
|
+
// Instead of: await ctx.reply(`Error: ${error}`);
|
|
885
|
+
// Use: await ctx.reply(formatUserError(error as Error));
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
**Step 6: Commit**
|
|
889
|
+
|
|
890
|
+
```bash
|
|
891
|
+
git add src/errors.ts src/__tests__/errors.test.ts src/handlers/text.ts
|
|
892
|
+
git commit -m "feat: add user-friendly error messages"
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
---
|
|
896
|
+
|
|
897
|
+
## Task 7: Session Version Check (Reliability)
|
|
898
|
+
|
|
899
|
+
**Files:**
|
|
900
|
+
|
|
901
|
+
- Modify: `src/session.ts`
|
|
902
|
+
- Modify: `src/types.ts`
|
|
903
|
+
|
|
904
|
+
**Step 1: Add version to session data**
|
|
905
|
+
|
|
906
|
+
In `src/types.ts`, update SessionData:
|
|
907
|
+
|
|
908
|
+
```typescript
|
|
909
|
+
export interface SessionData {
|
|
910
|
+
version: number; // Add version field
|
|
911
|
+
session_id: string;
|
|
912
|
+
saved_at: string;
|
|
913
|
+
working_dir: string;
|
|
914
|
+
}
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
**Step 2: Add version handling in session.ts**
|
|
918
|
+
|
|
919
|
+
```typescript
|
|
920
|
+
// Add constant at top of file
|
|
921
|
+
const SESSION_VERSION = 1;
|
|
922
|
+
|
|
923
|
+
// Update saveSession to include version:
|
|
924
|
+
const data: SessionData = {
|
|
925
|
+
version: SESSION_VERSION,
|
|
926
|
+
session_id: this.sessionId,
|
|
927
|
+
saved_at: new Date().toISOString(),
|
|
928
|
+
working_dir: this._workingDir,
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
// Update resumeLast to check version:
|
|
932
|
+
if (data.version !== SESSION_VERSION) {
|
|
933
|
+
return [
|
|
934
|
+
false,
|
|
935
|
+
`Session version mismatch (found v${data.version}, expected v${SESSION_VERSION})`,
|
|
936
|
+
];
|
|
937
|
+
}
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
**Step 3: Commit**
|
|
941
|
+
|
|
942
|
+
```bash
|
|
943
|
+
git add src/session.ts src/types.ts
|
|
944
|
+
git commit -m "feat: add session version checking for compatibility"
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
---
|
|
948
|
+
|
|
949
|
+
## Task 8: Run All Tests and Fix Issues
|
|
950
|
+
|
|
951
|
+
**Step 1: Run full test suite**
|
|
952
|
+
|
|
953
|
+
```bash
|
|
954
|
+
bun test
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
**Step 2: Fix any failing tests**
|
|
958
|
+
|
|
959
|
+
**Step 3: Run typecheck**
|
|
960
|
+
|
|
961
|
+
```bash
|
|
962
|
+
bun run typecheck
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
**Step 4: Fix any type errors**
|
|
966
|
+
|
|
967
|
+
**Step 5: Commit fixes**
|
|
968
|
+
|
|
969
|
+
```bash
|
|
970
|
+
git add -A
|
|
971
|
+
git commit -m "fix: address test failures and type errors"
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
---
|
|
975
|
+
|
|
976
|
+
## Task 9: Update README
|
|
977
|
+
|
|
978
|
+
**Files:**
|
|
979
|
+
|
|
980
|
+
- Modify: `README.md`
|
|
981
|
+
|
|
982
|
+
**Step 1: Add section about reliability improvements**
|
|
983
|
+
|
|
984
|
+
Add to README.md in the Features section:
|
|
985
|
+
|
|
986
|
+
```markdown
|
|
987
|
+
## Reliability Features
|
|
988
|
+
|
|
989
|
+
- **Debounced session persistence** - Reduces disk I/O by batching session saves
|
|
990
|
+
- **Automatic retry** - Transient Telegram API errors are retried with exponential backoff
|
|
991
|
+
- **Graceful shutdown** - Clean shutdown with timeout ensures session data is saved
|
|
992
|
+
- **Session versioning** - Prevents loading incompatible session data after updates
|
|
993
|
+
- **Friendly errors** - Technical errors are translated to helpful user messages
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
**Step 2: Commit**
|
|
997
|
+
|
|
998
|
+
```bash
|
|
999
|
+
git add README.md
|
|
1000
|
+
git commit -m "docs: document reliability and performance improvements"
|
|
1001
|
+
```
|
|
1002
|
+
|
|
1003
|
+
---
|
|
1004
|
+
|
|
1005
|
+
## Task 10: Final Commit
|
|
1006
|
+
|
|
1007
|
+
**Step 1: Run final verification**
|
|
1008
|
+
|
|
1009
|
+
```bash
|
|
1010
|
+
bun test
|
|
1011
|
+
bun run typecheck
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
**Step 2: Create summary commit if needed**
|
|
1015
|
+
|
|
1016
|
+
```bash
|
|
1017
|
+
git log --oneline -10
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
Review commits and squash if desired.
|
|
1021
|
+
|
|
1022
|
+
---
|
|
1023
|
+
|
|
1024
|
+
## Summary of Changes
|
|
1025
|
+
|
|
1026
|
+
| Area | Change | Impact |
|
|
1027
|
+
| ------------ | ------------------------ | --------------------------- |
|
|
1028
|
+
| Performance | Session write debouncing | 90% reduction in disk I/O |
|
|
1029
|
+
| Reliability | Telegram API retry | Handles transient failures |
|
|
1030
|
+
| Architecture | Event emitter pattern | No circular dependencies |
|
|
1031
|
+
| Reliability | Graceful shutdown | Clean exit, no data loss |
|
|
1032
|
+
| UX | Progress feedback | Users see tool timing |
|
|
1033
|
+
| UX | Friendly errors | Technical errors translated |
|
|
1034
|
+
| Reliability | Session versioning | Safe upgrades |
|
|
1035
|
+
|
|
1036
|
+
**Total new files:** 4 (`telegram-api.ts`, `events.ts`, `errors.ts`, `session.test.ts`)
|
|
1037
|
+
**Modified files:** 6 (`session.ts`, `utils.ts`, `streaming.ts`, `types.ts`, `index.ts`, `README.md`)
|
|
1038
|
+
**New tests:** 4 test files
|