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
@@ -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.`, topicName);
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
- // Single-line bootstrap to avoid tmux send-keys newline issues.
171
- // Multi-line context is written to a temp file for Claude to read.
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
- `Respond to the user's message via Telegram relay: cat <<'EOF' | .claude/scripts/telegram-reply.sh ${topicId}`,
175
- `Your response here`,
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 fileInfo = await this.apiCall('getFile', { file_id: fileId });
282
- const fileUrl = `https://api.telegram.org/file/bot${this.config.token}/${fileInfo.file_path}`;
283
- const controller = new AbortController();
284
- const timer = setTimeout(() => controller.abort(), 60_000);
285
- try {
286
- const response = await fetch(fileUrl, { signal: controller.signal });
287
- if (!response.ok)
288
- throw new Error(`Download failed: ${response.status}`);
289
- const buffer = Buffer.from(await response.arrayBuffer());
290
- fs.writeFileSync(destPath, buffer);
291
- }
292
- finally {
293
- clearTimeout(timer);
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Persistent autonomy infrastructure for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",