instar 1.3.445 → 1.3.446

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.
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAuBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAiG7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA05BD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CA0UN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAyvUtE;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;AAaH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAuBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAiG7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA05BD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CA0UN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAoyUtE;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"}
@@ -4373,6 +4373,20 @@ export async function startServer(options) {
4373
4373
  const channelId = message.channel.identifier;
4374
4374
  const isDM = message.metadata?.isDM;
4375
4375
  const senderName = message.metadata?.senderName || 'User';
4376
+ // ── Thread → session routing (§5.3) ──────────────────────────────────
4377
+ // The Slack channelId is ALWAYS the address we talk to the Slack API with
4378
+ // (replies, reactions, history). The session REGISTRY + resume map are
4379
+ // keyed on a routing key that, when thread routing is opted in for this
4380
+ // channel and the message is a reply inside a thread, becomes
4381
+ // `<channelId>:<thread_ts>` — giving that thread its own isolated session,
4382
+ // mirroring Telegram topic→session. Default (no opt-in / no thread_ts):
4383
+ // routingKey === channelId, byte-for-byte today's behavior.
4384
+ const messageTs = message.metadata?.ts;
4385
+ const threadTs = message.metadata?.threadTs;
4386
+ const routingKey = slackAdapter.resolveRoutingKey(channelId, threadTs, messageTs);
4387
+ const isThreadSession = slackAdapter.isThreadRoutingKey(routingKey);
4388
+ // The thread_ts to thread replies under (only when this is a thread session).
4389
+ const replyThreadTs = isThreadSession ? threadTs : undefined;
4376
4390
  // Sentinel intercept — classify message for emergency stop/pause
4377
4391
  if (sentinel) {
4378
4392
  try {
@@ -4390,7 +4404,7 @@ export async function startServer(options) {
4390
4404
  return;
4391
4405
  }
4392
4406
  else if (classification.category === 'pause') {
4393
- const existingSession = slackAdapter.getSessionForChannel(channelId);
4407
+ const existingSession = slackAdapter.getSessionForChannel(routingKey);
4394
4408
  if (existingSession) {
4395
4409
  sessionManager.sendKey(existingSession, 'Escape');
4396
4410
  slackAdapter.sendToChannel(channelId, '⏸️ Session paused.').catch(() => { });
@@ -4447,9 +4461,16 @@ export async function startServer(options) {
4447
4461
  contextLines.push('CRITICAL: You MUST relay your response back to Slack after responding.');
4448
4462
  contextLines.push('Use the relay script (write ONLY your reply text — do NOT pipe or cat this file into the script):');
4449
4463
  contextLines.push('');
4450
- contextLines.push(`cat <<'EOF' | .claude/scripts/slack-reply.sh ${channelId}`);
4464
+ // Thread session: pass the thread_ts as the 2nd arg so the reply lands IN
4465
+ // the thread (not the channel root). Channel session: channelId only.
4466
+ const replyTarget = replyThreadTs ? `${channelId} ${replyThreadTs}` : `${channelId}`;
4467
+ contextLines.push(`cat <<'EOF' | .claude/scripts/slack-reply.sh ${replyTarget}`);
4451
4468
  contextLines.push('Your response text here');
4452
4469
  contextLines.push('EOF');
4470
+ if (replyThreadTs) {
4471
+ contextLines.push('');
4472
+ contextLines.push('(This is a THREAD conversation — keep your reply in this thread by passing the thread id shown above as the 2nd argument.)');
4473
+ }
4453
4474
  contextLines.push('');
4454
4475
  contextLines.push('Strip the [slack:] prefix before interpreting the message.');
4455
4476
  contextLines.push('Only relay conversational text — not tool output, file contents, or internal reasoning.');
@@ -4485,8 +4506,8 @@ export async function startServer(options) {
4485
4506
  else {
4486
4507
  bootstrapMessage = fullMessage;
4487
4508
  }
4488
- // Check for existing session bound to this channel
4489
- const existingSession = slackAdapter.getSessionForChannel(channelId);
4509
+ // Check for existing session bound to this channel/thread (routing key)
4510
+ const existingSession = slackAdapter.getSessionForChannel(routingKey);
4490
4511
  if (existingSession) {
4491
4512
  // Try to inject into existing session via tmux
4492
4513
  const sessions = sessionManager.listRunningSessions();
@@ -4524,20 +4545,26 @@ export async function startServer(options) {
4524
4545
  }
4525
4546
  console.log(`[slack→session] Session "${existingSession}" died, respawning...`);
4526
4547
  }
4527
- // Check resume map for session continuity
4528
- const resumeInfo = slackAdapter.getChannelResume(channelId);
4548
+ // Check resume map for session continuity (keyed on routing key, so a
4549
+ // thread resumes its OWN session and not the channel-root one).
4550
+ const resumeInfo = slackAdapter.getChannelResume(routingKey);
4529
4551
  const resumeSessionId = resumeInfo?.uuid ?? undefined;
4530
4552
  if (resumeInfo) {
4531
- slackAdapter.removeChannelResume(channelId);
4553
+ slackAdapter.removeChannelResume(routingKey);
4532
4554
  }
4533
- // Route: DMs go to lifeline session, channels spawn new sessions
4534
- const targetSession = isDM ? 'lifeline' : undefined;
4555
+ // Route: DMs go to lifeline session, channels/threads spawn new sessions.
4556
+ // A thread session NEVER folds into the DM lifeline it is its own
4557
+ // isolated work session (DMs don't carry thread_ts anyway).
4558
+ const targetSession = (isDM && !isThreadSession) ? 'lifeline' : undefined;
4535
4559
  try {
4536
- const newSessionName = await sessionManager.spawnInteractiveSession(bootstrapMessage, targetSession, { resumeSessionId, slackChannelId: channelId });
4560
+ const newSessionName = await sessionManager.spawnInteractiveSession(bootstrapMessage, targetSession, { resumeSessionId, slackChannelId: channelId, slackThreadTs: replyThreadTs });
4537
4561
  if (newSessionName) {
4538
- slackAdapter.registerChannelSession(channelId, newSessionName);
4562
+ // Register on the routing key (channelId for a channel session,
4563
+ // `<channelId>:<thread_ts>` for a thread session). channelName carries
4564
+ // a thread hint so the registry stays human-readable.
4565
+ slackAdapter.registerChannelSession(routingKey, newSessionName, isThreadSession ? `${channelId} (thread ${replyThreadTs})` : undefined);
4539
4566
  slackAdapter.trackMessageInjection(channelId, newSessionName, message.content);
4540
- console.log(`[slack→session] ${resumeSessionId ? 'Resumed' : 'Spawned'} "${newSessionName}" for channel ${channelId}`);
4567
+ console.log(`[slack→session] ${resumeSessionId ? 'Resumed' : 'Spawned'} "${newSessionName}" for ${isThreadSession ? `thread ${routingKey}` : `channel ${channelId}`}`);
4541
4568
  }
4542
4569
  }
4543
4570
  catch (err) {
@@ -5550,17 +5577,24 @@ export async function startServer(options) {
5550
5577
  }
5551
5578
  console.log(`[respawnSessionFresh] topicId=${topicId} slackChId=${slackChId || 'none'} slackAdapter=${!!_slackAdapter} session=${_sessionName} mapSize=${slackProxyChannelMap.size}`);
5552
5579
  if (slackChId && _slackAdapter) {
5580
+ // Thread→session (§5.3): slackChId may be a routing key
5581
+ // (`<channelId>:<thread_ts>`). The registry + resume map are keyed on
5582
+ // the routing key (slackChId); the Slack API + reply instruction need
5583
+ // the RAW channel id, and a thread reply needs the embedded thread_ts.
5584
+ const parsedTarget = _slackAdapter.parseRoutingKey(slackChId);
5585
+ const slackApiChannel = parsedTarget.channelId;
5586
+ const slackReplyThread = parsedTarget.threadTs;
5553
5587
  // Kill existing session (already flagged in contextExhaustionKills via event listener)
5554
5588
  const session = sessionManager.listRunningSessions().find(s => s.tmuxSession === _sessionName);
5555
5589
  if (session)
5556
5590
  sessionManager.killSession(session.id);
5557
- // Clear the channel resume so the new session starts fresh
5591
+ // Clear the channel/thread resume so the new session starts fresh
5558
5592
  _slackAdapter.removeChannelResume(slackChId);
5559
5593
  // Spawn a fresh session with recovery context
5560
5594
  await new Promise(resolve => setTimeout(resolve, 2000));
5561
5595
  // Build a recovery bootstrap message with thread history (inline, matching Telegram pattern)
5562
5596
  // Use async fallback to fetch from Slack API if ring buffer is empty (race condition on restart)
5563
- const history = await _slackAdapter.getChannelMessagesWithFallback(slackChId, 30);
5597
+ const history = await _slackAdapter.getChannelMessagesWithFallback(slackApiChannel, 30);
5564
5598
  const botUserId = _slackAdapter.getBotUserId?.() ?? null;
5565
5599
  const lines = [];
5566
5600
  lines.push(`CONTINUATION — You are resuming an EXISTING Slack conversation after context exhaustion. Read the context below and pick up where you left off. Do NOT ask what was being discussed.`);
@@ -5581,25 +5615,28 @@ export async function startServer(options) {
5581
5615
  lines.push('--- End Thread History ---');
5582
5616
  }
5583
5617
  else {
5584
- console.warn(`[slack→recovery] WARNING: No history available for channel ${slackChId} — recovery context is empty. Ring buffer may not be populated yet.`);
5618
+ console.warn(`[slack→recovery] WARNING: No history available for channel ${slackApiChannel} — recovery context is empty. Ring buffer may not be populated yet.`);
5585
5619
  lines.push('[WARNING: Thread history unavailable — ring buffer may not be populated. Check Slack channel for recent messages before responding.]');
5586
5620
  }
5587
5621
  lines.push('');
5588
5622
  lines.push('CRITICAL: You MUST relay your response back to Slack.');
5589
- lines.push(`cat <<'EOF' | .claude/scripts/slack-reply.sh ${slackChId}`);
5623
+ // Thread session: include the thread_ts so the recovered reply threads.
5624
+ const recoveryReplyTarget = slackReplyThread ? `${slackApiChannel} ${slackReplyThread}` : `${slackApiChannel}`;
5625
+ lines.push(`cat <<'EOF' | .claude/scripts/slack-reply.sh ${recoveryReplyTarget}`);
5590
5626
  lines.push('Your response text here');
5591
5627
  lines.push('EOF');
5592
5628
  const tmpDir = '/tmp/instar-slack';
5593
5629
  fs.mkdirSync(tmpDir, { recursive: true });
5594
- const ctxPath = path.join(tmpDir, `recovery-${slackChId}-${Date.now()}.txt`);
5630
+ const ctxPath = path.join(tmpDir, `recovery-${slackApiChannel}-${Date.now()}.txt`);
5595
5631
  const contextData = lines.join('\n');
5596
5632
  fs.writeFileSync(ctxPath, contextData);
5597
- const bootstrapMessage = `[slack:${slackChId}] ${contextData}`;
5633
+ const bootstrapMessage = `[slack:${slackApiChannel}] ${contextData}`;
5598
5634
  try {
5599
- const newSessionName = await sessionManager.spawnInteractiveSession(bootstrapMessage, undefined, { slackChannelId: slackChId });
5635
+ // Spawn with the RAW channel (+ thread_ts) but register on the routing key.
5636
+ const newSessionName = await sessionManager.spawnInteractiveSession(bootstrapMessage, undefined, { slackChannelId: slackApiChannel, slackThreadTs: slackReplyThread });
5600
5637
  if (newSessionName) {
5601
5638
  _slackAdapter.registerChannelSession(slackChId, newSessionName);
5602
- console.log(`[slack→recovery] Fresh session "${newSessionName}" spawned for channel ${slackChId} (context exhaustion recovery)`);
5639
+ console.log(`[slack→recovery] Fresh session "${newSessionName}" spawned for ${slackReplyThread ? `thread ${slackChId}` : `channel ${slackApiChannel}`} (context exhaustion recovery)`);
5603
5640
  }
5604
5641
  }
5605
5642
  catch (err) {