instar 0.4.2 → 0.4.4
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/cli.js
CHANGED
|
File without changes
|
package/dist/commands/server.js
CHANGED
|
@@ -55,15 +55,16 @@ async function respawnSessionForTopic(sessionManager, telegram, targetSession, t
|
|
|
55
55
|
const tmpDir = '/tmp/instar-telegram';
|
|
56
56
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
57
57
|
let bootstrapMessage;
|
|
58
|
+
const relayNote = `You MUST relay your response via: cat <<'EOF' | .claude/scripts/telegram-reply.sh ${topicId}\nYour response\nEOF`;
|
|
58
59
|
if (historyLines.length > 0) {
|
|
59
60
|
const historyContent = historyLines.join('\n');
|
|
60
61
|
const filepath = path.join(tmpDir, `history-${topicId}-${Date.now()}-${process.pid}.txt`);
|
|
61
62
|
fs.writeFileSync(filepath, historyContent);
|
|
62
|
-
// Single-line: user message + file reference for history
|
|
63
|
-
bootstrapMessage = `[telegram:${topicId}] ${msg} (Session respawned. Thread history at ${filepath} — read it for context before responding.)`;
|
|
63
|
+
// Single-line: user message + file reference for history + relay instructions
|
|
64
|
+
bootstrapMessage = `[telegram:${topicId}] ${msg} (Session respawned. Thread history at ${filepath} — read it for context before responding. ${relayNote})`;
|
|
64
65
|
}
|
|
65
66
|
else {
|
|
66
|
-
bootstrapMessage = `[telegram:${topicId}] ${msg}`;
|
|
67
|
+
bootstrapMessage = `[telegram:${topicId}] ${msg} (${relayNote})`;
|
|
67
68
|
}
|
|
68
69
|
const storedName = telegram.getTopicName(topicId);
|
|
69
70
|
const topicName = storedName || targetSession;
|
|
@@ -113,6 +114,27 @@ function wireTelegramCallbacks(telegram, sessionManager, state) {
|
|
|
113
114
|
telegram.onIsSessionAlive = (sessionName) => {
|
|
114
115
|
return sessionManager.isSessionAlive(sessionName);
|
|
115
116
|
};
|
|
117
|
+
// Stall verification — check if session has recent output activity
|
|
118
|
+
telegram.onIsSessionActive = async (sessionName) => {
|
|
119
|
+
const output = sessionManager.captureOutput(sessionName, 20);
|
|
120
|
+
if (!output)
|
|
121
|
+
return false;
|
|
122
|
+
const lines = output.trim().split('\n').slice(-15);
|
|
123
|
+
// Look for signs of Claude Code activity in recent output
|
|
124
|
+
const activePatterns = [
|
|
125
|
+
/\bRead\b|\bWrite\b|\bEdit\b|\bBash\b|\bGrep\b|\bGlob\b/, // Tool names
|
|
126
|
+
/⠋|⠙|⠹|⠸|⠼|⠴|⠦|⠧|⠇|⠏/, // Spinner characters
|
|
127
|
+
/\d+\s*tokens?/i, // Token counts
|
|
128
|
+
/Sent \d+ chars/, // Telegram reply confirmation
|
|
129
|
+
];
|
|
130
|
+
for (const line of lines) {
|
|
131
|
+
for (const pattern of activePatterns) {
|
|
132
|
+
if (pattern.test(line))
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
};
|
|
116
138
|
}
|
|
117
139
|
/**
|
|
118
140
|
* Wire up Telegram message routing: topic messages → Claude sessions.
|
|
@@ -133,7 +155,7 @@ function wireTelegramRouting(telegram, sessionManager) {
|
|
|
133
155
|
(async () => {
|
|
134
156
|
try {
|
|
135
157
|
const topic = await telegram.createForumTopic(topicName, 9367192); // Green
|
|
136
|
-
const newSession = await sessionManager.spawnInteractiveSession(`[telegram:${topic.topicId}] New session started
|
|
158
|
+
const newSession = await sessionManager.spawnInteractiveSession(`[telegram:${topic.topicId}] New session started. (IMPORTANT: Relay all responses back via: cat <<'EOF' | .claude/scripts/telegram-reply.sh ${topic.topicId}\nYour response\nEOF)`, topicName);
|
|
137
159
|
telegram.registerTopicSession(topic.topicId, newSession);
|
|
138
160
|
await telegram.sendToTopic(topic.topicId, `Session created. I'm here.`);
|
|
139
161
|
await telegram.sendToTopic(topicId, `New session created: "${topicName}" — check the new topic above.`);
|
|
@@ -167,19 +189,26 @@ function wireTelegramRouting(telegram, sessionManager) {
|
|
|
167
189
|
// No session mapped — auto-spawn one
|
|
168
190
|
console.log(`[telegram→session] No session for topic ${topicId}, auto-spawning...`);
|
|
169
191
|
const storedName = telegram.getTopicName(topicId) || `topic-${topicId}`;
|
|
170
|
-
//
|
|
171
|
-
//
|
|
192
|
+
// Write relay instructions to a temp file and reference it in the bootstrap message.
|
|
193
|
+
// The session needs to know HOW to respond back to Telegram.
|
|
172
194
|
const contextLines = [
|
|
173
195
|
`This session was auto-created for Telegram topic ${topicId}.`,
|
|
174
|
-
|
|
175
|
-
`
|
|
196
|
+
``,
|
|
197
|
+
`CRITICAL: You MUST relay your response back to Telegram after responding.`,
|
|
198
|
+
`Use the relay script:`,
|
|
199
|
+
``,
|
|
200
|
+
`cat <<'EOF' | .claude/scripts/telegram-reply.sh ${topicId}`,
|
|
201
|
+
`Your response text here`,
|
|
176
202
|
`EOF`,
|
|
203
|
+
``,
|
|
204
|
+
`Strip the [telegram:${topicId}] prefix before interpreting the message.`,
|
|
205
|
+
`Only relay conversational text — not tool output or internal reasoning.`,
|
|
177
206
|
];
|
|
178
207
|
const tmpDir = '/tmp/instar-telegram';
|
|
179
208
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
180
209
|
const ctxPath = path.join(tmpDir, `ctx-${topicId}-${Date.now()}.txt`);
|
|
181
210
|
fs.writeFileSync(ctxPath, contextLines.join('\n'));
|
|
182
|
-
const bootstrapMessage = `[telegram:${topicId}] ${text}`;
|
|
211
|
+
const bootstrapMessage = `[telegram:${topicId}] ${text} (IMPORTANT: Read ${ctxPath} for Telegram relay instructions — you MUST relay your response back.)`;
|
|
183
212
|
sessionManager.spawnInteractiveSession(bootstrapMessage, storedName).then((newSessionName) => {
|
|
184
213
|
telegram.registerTopicSession(topicId, newSessionName);
|
|
185
214
|
telegram.sendToTopic(topicId, `Session created.`).catch(() => { });
|
|
@@ -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.
|