instar 0.4.2 → 0.4.3
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/commands/server.js
CHANGED
|
@@ -113,6 +113,27 @@ function wireTelegramCallbacks(telegram, sessionManager, state) {
|
|
|
113
113
|
telegram.onIsSessionAlive = (sessionName) => {
|
|
114
114
|
return sessionManager.isSessionAlive(sessionName);
|
|
115
115
|
};
|
|
116
|
+
// Stall verification — check if session has recent output activity
|
|
117
|
+
telegram.onIsSessionActive = async (sessionName) => {
|
|
118
|
+
const output = sessionManager.captureOutput(sessionName, 20);
|
|
119
|
+
if (!output)
|
|
120
|
+
return false;
|
|
121
|
+
const lines = output.trim().split('\n').slice(-15);
|
|
122
|
+
// Look for signs of Claude Code activity in recent output
|
|
123
|
+
const activePatterns = [
|
|
124
|
+
/\bRead\b|\bWrite\b|\bEdit\b|\bBash\b|\bGrep\b|\bGlob\b/, // Tool names
|
|
125
|
+
/⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏/, // Spinner characters
|
|
126
|
+
/\d+\s*tokens?/i, // Token counts
|
|
127
|
+
/Sent \d+ chars/, // Telegram reply confirmation
|
|
128
|
+
];
|
|
129
|
+
for (const line of lines) {
|
|
130
|
+
for (const pattern of activePatterns) {
|
|
131
|
+
if (pattern.test(line))
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
};
|
|
116
137
|
}
|
|
117
138
|
/**
|
|
118
139
|
* Wire up Telegram message routing: topic messages → Claude sessions.
|
|
@@ -65,6 +65,7 @@ export declare class TelegramAdapter implements MessagingAdapter {
|
|
|
65
65
|
alive: boolean;
|
|
66
66
|
}>) | null;
|
|
67
67
|
onIsSessionAlive: ((tmuxSession: string) => boolean) | null;
|
|
68
|
+
onIsSessionActive: ((tmuxSession: string) => Promise<boolean>) | null;
|
|
68
69
|
constructor(config: TelegramConfig, stateDir: string);
|
|
69
70
|
start(): Promise<void>;
|
|
70
71
|
stop(): Promise<void>;
|
|
@@ -37,6 +37,7 @@ export class TelegramAdapter {
|
|
|
37
37
|
onRestartSession = null;
|
|
38
38
|
onListSessions = null;
|
|
39
39
|
onIsSessionAlive = null;
|
|
40
|
+
onIsSessionActive = null;
|
|
40
41
|
constructor(config, stateDir) {
|
|
41
42
|
this.config = config;
|
|
42
43
|
this.stateDir = stateDir;
|
|
@@ -237,7 +238,7 @@ export class TelegramAdapter {
|
|
|
237
238
|
}
|
|
238
239
|
}
|
|
239
240
|
}
|
|
240
|
-
checkForStalls() {
|
|
241
|
+
async checkForStalls() {
|
|
241
242
|
const stallMinutes = this.config.stallTimeoutMinutes ?? 5;
|
|
242
243
|
const stallThresholdMs = stallMinutes * 60 * 1000;
|
|
243
244
|
const now = Date.now();
|
|
@@ -250,6 +251,21 @@ export class TelegramAdapter {
|
|
|
250
251
|
const alive = this.onIsSessionAlive
|
|
251
252
|
? this.onIsSessionAlive(pending.sessionName)
|
|
252
253
|
: true; // assume alive if no checker
|
|
254
|
+
// If alive, verify the session is truly stalled (not just responding through a different path)
|
|
255
|
+
if (alive && this.onIsSessionActive) {
|
|
256
|
+
try {
|
|
257
|
+
const active = await this.onIsSessionActive(pending.sessionName);
|
|
258
|
+
if (active) {
|
|
259
|
+
// Session is producing output — false alarm, clear it
|
|
260
|
+
console.log(`[telegram] Session "${pending.sessionName}" verified active, clearing stall`);
|
|
261
|
+
this.pendingMessages.delete(key);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// Verifier failed — fall through to alert
|
|
267
|
+
}
|
|
268
|
+
}
|
|
253
269
|
pending.alerted = true;
|
|
254
270
|
const status = alive ? 'running but not responding' : 'no longer running';
|
|
255
271
|
const minutesAgo = Math.round((now - pending.injectedAt) / 60000);
|
|
@@ -278,20 +294,36 @@ export class TelegramAdapter {
|
|
|
278
294
|
* Download a file from Telegram by file_id.
|
|
279
295
|
*/
|
|
280
296
|
async downloadFile(fileId, destPath) {
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
297
|
+
const maxRetries = 3;
|
|
298
|
+
let lastError;
|
|
299
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
300
|
+
try {
|
|
301
|
+
const fileInfo = await this.apiCall('getFile', { file_id: fileId });
|
|
302
|
+
const fileUrl = `https://api.telegram.org/file/bot${this.config.token}/${fileInfo.file_path}`;
|
|
303
|
+
const controller = new AbortController();
|
|
304
|
+
const timer = setTimeout(() => controller.abort(), 60_000);
|
|
305
|
+
try {
|
|
306
|
+
const response = await fetch(fileUrl, { signal: controller.signal });
|
|
307
|
+
if (!response.ok)
|
|
308
|
+
throw new Error(`Download failed: ${response.status}`);
|
|
309
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
310
|
+
fs.writeFileSync(destPath, buffer);
|
|
311
|
+
return; // Success
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
clearTimeout(timer);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
319
|
+
if (attempt < maxRetries) {
|
|
320
|
+
const delay = attempt * 1000;
|
|
321
|
+
console.warn(`[telegram] File download attempt ${attempt}/${maxRetries} failed, retrying in ${delay}ms...`);
|
|
322
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
294
325
|
}
|
|
326
|
+
throw lastError;
|
|
295
327
|
}
|
|
296
328
|
/**
|
|
297
329
|
* Resolve voice transcription provider from config or environment.
|