instar 0.24.33 → 0.25.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/dist/cli.js CHANGED
File without changes
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA0PH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA+lCD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAurGtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA0PH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAsnCD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAu2GtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
@@ -1019,6 +1019,24 @@ async function ensureSlackAttentionChannel(slack, state) {
1019
1019
  console.error(` Failed to create Slack Attention channel: ${err}`);
1020
1020
  }
1021
1021
  }
1022
+ /**
1023
+ * Ensure a Slack updates channel exists — for version updates and feature announcements.
1024
+ */
1025
+ async function ensureSlackUpdatesChannel(slack, state) {
1026
+ const existingChannelId = state.get('slack-updates-channel');
1027
+ if (existingChannelId)
1028
+ return;
1029
+ try {
1030
+ const agentName = slack.config?.workspaceName?.replace(/-agent$/, '') || 'agent';
1031
+ const channelId = await slack.createChannel(`${agentName}-sys-updates`);
1032
+ state.set('slack-updates-channel', channelId);
1033
+ await slack.sendToChannel(channelId, `Updates channel active. Version updates, new features, and system announcements will appear here.`);
1034
+ console.log(` Created Slack Updates channel: ${channelId}`);
1035
+ }
1036
+ catch (err) {
1037
+ console.error(` Failed to create Slack Updates channel: ${err}`);
1038
+ }
1039
+ }
1022
1040
  /**
1023
1041
  * Ensure the Agent Updates topic exists — for version updates, feature announcements, etc.
1024
1042
  * Separates informational updates from critical attention items.
@@ -1259,8 +1277,8 @@ export async function startServer(options) {
1259
1277
  topicId: resolvedTopicId,
1260
1278
  }).catch(() => { });
1261
1279
  }
1262
- // Slack: send IMMEDIATE notifications to attention channel (batching deferred to future)
1263
- if (tier === 'IMMEDIATE' && _slackAdapter) {
1280
+ // Slack: send all notification tiers to attention channel
1281
+ if (_slackAdapter) {
1264
1282
  const slackAttentionChannel = _notifyState?.get('slack-attention-channel');
1265
1283
  if (slackAttentionChannel) {
1266
1284
  _slackAdapter.sendToChannel(slackAttentionChannel, message).catch(() => { });
@@ -2218,8 +2236,46 @@ export async function startServer(options) {
2218
2236
  const channelId = message.channel.identifier;
2219
2237
  const isDM = message.metadata?.isDM;
2220
2238
  const senderName = message.metadata?.senderName || 'User';
2221
- // Build injection tag
2222
- const prefix = `[slack:${channelId}]`;
2239
+ // Sentinel intercept — classify message for emergency stop/pause
2240
+ if (sentinel) {
2241
+ try {
2242
+ const classification = await sentinel.classify(message.content);
2243
+ if (classification.category === 'emergency-stop') {
2244
+ // Kill all sessions
2245
+ const sessions = sessionManager.listRunningSessions();
2246
+ for (const s of sessions) {
2247
+ try {
2248
+ sessionManager.killSession(s.id);
2249
+ }
2250
+ catch { /* ok */ }
2251
+ }
2252
+ slackAdapter.sendToChannel(channelId, '🛑 Emergency stop — all sessions killed.').catch(() => { });
2253
+ return;
2254
+ }
2255
+ else if (classification.category === 'pause') {
2256
+ const existingSession = slackAdapter.getSessionForChannel(channelId);
2257
+ if (existingSession) {
2258
+ sessionManager.sendKey(existingSession, 'Escape');
2259
+ slackAdapter.sendToChannel(channelId, '⏸️ Session paused.').catch(() => { });
2260
+ }
2261
+ return;
2262
+ }
2263
+ }
2264
+ catch { /* fail-open — if Sentinel errors, process message normally */ }
2265
+ }
2266
+ // Build injection tag with sender info (matches Telegram's buildInjectionTag pattern)
2267
+ const slackUserId = message.metadata?.slackUserId;
2268
+ // Sanitize sender name at injection boundary (prevents injection attacks)
2269
+ const safeSenderName = senderName
2270
+ ? senderName.replace(/[\x00-\x1f\x7f]/g, '').replace(/\s+/g, ' ').replace(/["\[\]]/g, '').trim().slice(0, 64) || 'User'
2271
+ : undefined;
2272
+ let prefix = `[slack:${channelId}]`;
2273
+ if (safeSenderName && slackUserId) {
2274
+ prefix = `[slack:${channelId} from ${safeSenderName} (uid:${slackUserId})]`;
2275
+ }
2276
+ else if (safeSenderName) {
2277
+ prefix = `[slack:${channelId} from ${safeSenderName}]`;
2278
+ }
2223
2279
  // Write context file for the session
2224
2280
  const tmpDir = '/tmp/instar-slack';
2225
2281
  fs.mkdirSync(tmpDir, { recursive: true });
@@ -2267,7 +2323,18 @@ export async function startServer(options) {
2267
2323
  }
2268
2324
  return `[User sent a file — it has been saved to ${docPath}. Read the file to view its contents]`;
2269
2325
  });
2270
- const bootstrapMessage = `${prefix} ${transformedContent} (IMPORTANT: Read ${ctxPath} for thread history and Slack relay instructions — you MUST relay your response back.)`;
2326
+ const FILE_THRESHOLD = 500;
2327
+ const fullMessage = `${prefix} ${transformedContent} (IMPORTANT: Read ${ctxPath} for thread history and Slack relay instructions — you MUST relay your response back.)`;
2328
+ // Long messages: write to temp file and inject reference (matches Telegram pattern)
2329
+ let bootstrapMessage;
2330
+ if (fullMessage.length > FILE_THRESHOLD) {
2331
+ const msgFilePath = path.join(tmpDir, `msg-${channelId}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}.txt`);
2332
+ fs.writeFileSync(msgFilePath, fullMessage);
2333
+ bootstrapMessage = `${prefix} [Long message saved to ${msgFilePath} — read it to see the full message]`;
2334
+ }
2335
+ else {
2336
+ bootstrapMessage = fullMessage;
2337
+ }
2271
2338
  // Check for existing session bound to this channel
2272
2339
  const existingSession = slackAdapter.getSessionForChannel(channelId);
2273
2340
  if (existingSession) {
@@ -2292,13 +2359,13 @@ export async function startServer(options) {
2292
2359
  // Fall through to respawn below — registerChannelSession will be called with new session name
2293
2360
  }
2294
2361
  else {
2295
- // Session is ready — inject the message
2362
+ // Session is ready — inject via SessionManager (handles idle timer reset + bracketed paste)
2296
2363
  try {
2297
- const { execSync } = await import('node:child_process');
2298
- const msgFile = path.join(tmpDir, `inject-${Date.now()}.txt`);
2299
- fs.writeFileSync(msgFile, bootstrapMessage);
2300
- execSync(`tmux send-keys -t '=${existingSession}:' "$(cat '${msgFile}')" Enter`, { timeout: 5000 });
2301
- fs.unlinkSync(msgFile);
2364
+ sessionManager.injectMessage(existingSession, bootstrapMessage);
2365
+ // Track for stall detection
2366
+ slackAdapter.trackMessageInjection(channelId, existingSession, message.content);
2367
+ // Delivery confirmation
2368
+ slackAdapter.sendToChannel(channelId, '✓ Delivered').catch(() => { });
2302
2369
  }
2303
2370
  catch (injectErr) {
2304
2371
  console.error(`[slack→session] Injection failed: ${injectErr instanceof Error ? injectErr.message : injectErr}`);
@@ -2320,6 +2387,7 @@ export async function startServer(options) {
2320
2387
  const newSessionName = await sessionManager.spawnInteractiveSession(bootstrapMessage, targetSession, { resumeSessionId });
2321
2388
  if (newSessionName) {
2322
2389
  slackAdapter.registerChannelSession(channelId, newSessionName);
2390
+ slackAdapter.trackMessageInjection(channelId, newSessionName, message.content);
2323
2391
  console.log(`[slack→session] ${resumeSessionId ? 'Resumed' : 'Spawned'} "${newSessionName}" for channel ${channelId}`);
2324
2392
  }
2325
2393
  }
@@ -2330,17 +2398,100 @@ export async function startServer(options) {
2330
2398
  await slackAdapter.start();
2331
2399
  _slackAdapter = slackAdapter;
2332
2400
  console.log(pc.green(` Slack connected (workspace: ${slackConfig.config.workspaceName || 'unknown'})`));
2333
- // Ensure Slack attention channel exists
2401
+ // Ensure Slack system channels exist
2334
2402
  ensureSlackAttentionChannel(slackAdapter, state).catch(err => {
2335
2403
  console.error(`[server] Failed to ensure Slack Attention channel: ${err}`);
2336
2404
  });
2405
+ ensureSlackUpdatesChannel(slackAdapter, state).catch(err => {
2406
+ console.error(`[server] Failed to ensure Slack Updates channel: ${err}`);
2407
+ });
2337
2408
  // Wire stall detection — route stall alerts to Slack attention channel
2338
- slackAdapter.onStallDetected = (channelId, sessionName, messageText) => {
2409
+ slackAdapter.onStallDetected = (channelId, sessionName, messageText, injectedAt) => {
2339
2410
  const slackAttentionChannel = state.get('slack-attention-channel');
2340
2411
  if (slackAttentionChannel) {
2341
- slackAdapter.sendToChannel(slackAttentionChannel, `Stall detected in session "${sessionName}" (channel ${channelId}). Message may not have been answered: "${messageText.slice(0, 100)}"`).catch(() => { });
2412
+ slackAdapter.sendToChannel(slackAttentionChannel, `⚠️ Stall detected in session "${sessionName}" (channel ${channelId}). Message may not have been answered: "${messageText.slice(0, 100)}"`).catch(() => { });
2413
+ }
2414
+ // Also notify in the originating channel
2415
+ slackAdapter.sendToChannel(channelId, `⚠️ The session appears to have stalled. Use \`!restart\` to restart or \`!interrupt\` to nudge it.`).catch(() => { });
2416
+ };
2417
+ // Wire voice transcription (reuses Telegram's provider resolution: Groq → OpenAI)
2418
+ slackAdapter.transcribeVoice = async (filePath) => {
2419
+ const providers = {
2420
+ groq: { envKey: 'GROQ_API_KEY', baseUrl: 'https://api.groq.com/openai/v1', model: 'whisper-large-v3' },
2421
+ openai: { envKey: 'OPENAI_API_KEY', baseUrl: 'https://api.openai.com/v1', model: 'whisper-1' },
2422
+ };
2423
+ let provider = null;
2424
+ const explicit = slackConfig.config.audioTranscriptionProvider;
2425
+ if (explicit && providers[explicit.toLowerCase()]) {
2426
+ const p = providers[explicit.toLowerCase()];
2427
+ const apiKey = process.env[p.envKey];
2428
+ if (apiKey)
2429
+ provider = { apiKey, baseUrl: p.baseUrl, model: p.model };
2430
+ }
2431
+ if (!provider) {
2432
+ for (const [, p] of Object.entries(providers)) {
2433
+ const apiKey = process.env[p.envKey];
2434
+ if (apiKey) {
2435
+ provider = { apiKey, baseUrl: p.baseUrl, model: p.model };
2436
+ break;
2437
+ }
2438
+ }
2439
+ }
2440
+ if (!provider)
2441
+ throw new Error('No voice transcription provider. Set GROQ_API_KEY or OPENAI_API_KEY.');
2442
+ const formData = new FormData();
2443
+ const fileBuffer = fs.readFileSync(filePath);
2444
+ const blob = new Blob([fileBuffer], { type: 'audio/ogg' });
2445
+ formData.append('file', blob, path.basename(filePath));
2446
+ formData.append('model', provider.model);
2447
+ const controller = new AbortController();
2448
+ const timer = setTimeout(() => controller.abort(), 60_000);
2449
+ try {
2450
+ const response = await fetch(`${provider.baseUrl}/audio/transcriptions`, {
2451
+ method: 'POST',
2452
+ headers: { Authorization: `Bearer ${provider.apiKey}` },
2453
+ body: formData,
2454
+ signal: controller.signal,
2455
+ });
2456
+ if (!response.ok)
2457
+ throw new Error(`Transcription API error (${response.status}): ${await response.text()}`);
2458
+ const data = await response.json();
2459
+ return data.text;
2342
2460
  }
2461
+ finally {
2462
+ clearTimeout(timer);
2463
+ }
2464
+ };
2465
+ // Start stall detection timer
2466
+ const stallTimeout = slackConfig.config.stallTimeoutMinutes ?? 5;
2467
+ if (stallTimeout > 0) {
2468
+ slackAdapter.startStallDetection(stallTimeout * 60 * 1000);
2469
+ }
2470
+ // Wire session management callbacks for slash commands
2471
+ slackAdapter.onInterruptSession = async (sessionName) => {
2472
+ return sessionManager.sendKey(sessionName, 'Escape');
2473
+ };
2474
+ slackAdapter.onRestartSession = async (sessionName, channelId) => {
2475
+ try {
2476
+ const stuckSession = sessionManager.listRunningSessions().find(s => s.tmuxSession === sessionName);
2477
+ if (stuckSession) {
2478
+ sessionManager.killSession(stuckSession.id);
2479
+ }
2480
+ }
2481
+ catch { /* ok if already dead */ }
2482
+ };
2483
+ slackAdapter.onListSessions = () => {
2484
+ return sessionManager.listRunningSessions().map(s => ({
2485
+ name: s.name,
2486
+ tmuxSession: s.tmuxSession,
2487
+ status: s.status,
2488
+ alive: sessionManager.isSessionAlive(s.tmuxSession),
2489
+ }));
2343
2490
  };
2491
+ slackAdapter.onIsSessionAlive = (tmuxSession) => {
2492
+ return sessionManager.isSessionAlive(tmuxSession);
2493
+ };
2494
+ // Standby commands will be wired after PresenceProxy is initialized (below)
2344
2495
  }
2345
2496
  catch (err) {
2346
2497
  const reason = err instanceof Error ? err.message : String(err);
@@ -2453,14 +2604,25 @@ export async function startServer(options) {
2453
2604
  if (_topicResumeMap && telegram) {
2454
2605
  sessionManager.on('beforeSessionKill', (session) => {
2455
2606
  try {
2607
+ // Save Telegram topic resume UUID
2456
2608
  const topicId = telegram.getTopicForSession(session.tmuxSession);
2457
- if (!topicId)
2458
- return;
2459
- // Use authoritative claudeSessionId from hook events, fall back to mtime heuristic
2460
- const uuid = _topicResumeMap.findUuidForSession(session.tmuxSession, session.claudeSessionId ?? undefined);
2461
- if (uuid) {
2462
- _topicResumeMap.save(topicId, uuid, session.tmuxSession);
2463
- console.log(`[beforeSessionKill] Saved resume UUID ${uuid} for topic ${topicId} (session: ${session.name}, source: ${session.claudeSessionId ? 'hook' : 'mtime'})`);
2609
+ if (topicId) {
2610
+ const uuid = _topicResumeMap.findUuidForSession(session.tmuxSession, session.claudeSessionId ?? undefined);
2611
+ if (uuid) {
2612
+ _topicResumeMap.save(topicId, uuid, session.tmuxSession);
2613
+ console.log(`[beforeSessionKill] Saved resume UUID ${uuid} for topic ${topicId} (session: ${session.name}, source: ${session.claudeSessionId ? 'hook' : 'mtime'})`);
2614
+ }
2615
+ }
2616
+ // Save Slack channel resume UUID
2617
+ if (_slackAdapter) {
2618
+ const channelId = _slackAdapter.getChannelForSession(session.tmuxSession);
2619
+ if (channelId) {
2620
+ const uuid = _topicResumeMap.findUuidForSession(session.tmuxSession, session.claudeSessionId ?? undefined);
2621
+ if (uuid) {
2622
+ _slackAdapter.saveChannelResume(channelId, uuid, session.tmuxSession);
2623
+ console.log(`[beforeSessionKill] Saved Slack resume UUID ${uuid} for channel ${channelId} (session: ${session.name})`);
2624
+ }
2625
+ }
2464
2626
  }
2465
2627
  }
2466
2628
  catch (err) {
@@ -3095,6 +3257,14 @@ export async function startServer(options) {
3095
3257
  slackProxyChannelMap.set(syntheticId, channelId);
3096
3258
  return syntheticId;
3097
3259
  }
3260
+ // Pre-populate the Slack proxy channel map from existing channel registry
3261
+ // so PresenceProxy state recovery can resolve synthetic IDs on restart
3262
+ if (_slackAdapter) {
3263
+ const registry = _slackAdapter.getChannelRegistry();
3264
+ for (const channelId of Object.keys(registry)) {
3265
+ slackChannelToSyntheticId(channelId);
3266
+ }
3267
+ }
3098
3268
  let presenceProxy;
3099
3269
  if (sharedIntelligence && telegram) {
3100
3270
  try {
@@ -3224,18 +3394,35 @@ export async function startServer(options) {
3224
3394
  });
3225
3395
  };
3226
3396
  presenceProxy.start();
3227
- // Wire Slack's onMessageLogged into PresenceProxy (if Slack is active)
3397
+ // Wire Slack's onMessageLogged into PresenceProxy + TopicMemory (if Slack is active)
3228
3398
  if (_slackAdapter) {
3229
3399
  const existingSlackCallback = _slackAdapter.onMessageLogged;
3230
3400
  _slackAdapter.onMessageLogged = (entry) => {
3231
3401
  if (existingSlackCallback) {
3232
3402
  existingSlackCallback(entry);
3233
3403
  }
3404
+ // TopicMemory dual-write (matches Telegram's insertMessage pattern)
3405
+ // TopicMemory uses numeric topicId; for Slack we use the synthetic hash
3406
+ if (entry.channelId && topicMemory) {
3407
+ const synId = slackChannelToSyntheticId(String(entry.channelId));
3408
+ topicMemory.insertMessage({
3409
+ messageId: typeof entry.messageId === 'number' ? entry.messageId : 0,
3410
+ topicId: synId,
3411
+ text: entry.text,
3412
+ fromUser: entry.fromUser,
3413
+ timestamp: entry.timestamp,
3414
+ sessionName: entry.sessionName,
3415
+ senderName: entry.senderName,
3416
+ });
3417
+ }
3418
+ // Clear stall tracking when agent responds in this channel
3419
+ if (!entry.fromUser && entry.channelId) {
3420
+ _slackAdapter.clearStallTracking(String(entry.channelId));
3421
+ }
3234
3422
  // Convert Slack channelId to synthetic numeric ID for PresenceProxy
3235
3423
  if (!entry.channelId)
3236
3424
  return;
3237
3425
  const syntheticId = slackChannelToSyntheticId(String(entry.channelId));
3238
- console.log(`[slack-proxy] onMessageLogged: channel=${entry.channelId} syntheticId=${syntheticId} fromUser=${entry.fromUser} text="${(entry.text || '').slice(0, 40)}"`);
3239
3426
  presenceProxy.onMessageLogged({
3240
3427
  messageId: typeof entry.messageId === 'number' ? entry.messageId : parseInt(String(entry.messageId), 10) || 0,
3241
3428
  channelId: syntheticId.toString(),
@@ -3248,6 +3435,11 @@ export async function startServer(options) {
3248
3435
  });
3249
3436
  };
3250
3437
  console.log(pc.green(' Presence Proxy wired to Slack'));
3438
+ // Wire standby commands for Slack (unstick, quiet, resume, restart)
3439
+ _slackAdapter.onStandbyCommand = async (channelId, command, userId) => {
3440
+ const syntheticId = slackChannelToSyntheticId(channelId);
3441
+ return presenceProxy.handleCommand(syntheticId, command, parseInt(userId, 10) || 0);
3442
+ };
3251
3443
  }
3252
3444
  console.log(pc.green(' Presence Proxy enabled (🔭 [Standby])'));
3253
3445
  }