claude-slack-channel-bots 0.0.4 → 0.1.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/README.md CHANGED
@@ -185,7 +185,7 @@ Checks prerequisites, then daemonizes the server.
185
185
  3. `SLACK_APP_TOKEN` is set — fails with `missing prerequisite: SLACK_APP_TOKEN environment variable` if absent.
186
186
  4. `routing.json` exists at `STATE_DIR/routing.json` — fails with the full path if not found.
187
187
 
188
- If all checks pass, the parent process spawns a detached child process and exits immediately, printing the child PID. The child starts the server and writes its PID to `STATE_DIR/server.pid`.
188
+ If all checks pass, the parent process spawns a detached child process and exits immediately, printing the child PID. The child starts the server and writes its PID to `STATE_DIR/server.pid`. Conversation context is preserved across server restarts when possible.
189
189
 
190
190
  ```
191
191
  [slack] Server starting in background (PID 12345)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-slack-channel-bots",
3
- "version": "0.0.4",
3
+ "version": "0.1.0",
4
4
  "description": "Multi-session Slack-to-Claude bridge — run multiple Claude Code bots across Slack channels via Socket Mode",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.ts CHANGED
File without changes
package/src/config.ts CHANGED
@@ -12,6 +12,12 @@ import { readFileSync } from 'fs'
12
12
  import { homedir } from 'os'
13
13
  import { resolve } from 'path'
14
14
 
15
+ // ---------------------------------------------------------------------------
16
+ // Constants
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export const MCP_SERVER_NAME = 'slack-channel-router'
20
+
15
21
  // ---------------------------------------------------------------------------
16
22
  // Types
17
23
  // ---------------------------------------------------------------------------
@@ -13,6 +13,7 @@ import { existsSync, mkdirSync, writeFileSync } from 'fs'
13
13
  import { homedir } from 'os'
14
14
  import { dirname, join } from 'path'
15
15
  import { defaultAccess } from './lib.ts'
16
+ import { MCP_SERVER_NAME } from './config.ts'
16
17
 
17
18
  // ---------------------------------------------------------------------------
18
19
  // Types
@@ -70,7 +71,7 @@ export function runPostinstall(options: PostinstallOptions = {}): void {
70
71
  } else {
71
72
  const skeleton = {
72
73
  mcpServers: {
73
- 'slack-channel-router': {
74
+ [MCP_SERVER_NAME]: {
74
75
  type: 'http',
75
76
  url: 'http://127.0.0.1:3100/mcp',
76
77
  },
package/src/registry.ts CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  CallToolRequestSchema,
15
15
  ListToolsRequestSchema,
16
16
  } from '@modelcontextprotocol/sdk/types.js'
17
- import type { RoutingConfig } from './config.ts'
17
+ import { MCP_SERVER_NAME, type RoutingConfig } from './config.ts'
18
18
 
19
19
  // ---------------------------------------------------------------------------
20
20
  // Types
@@ -335,7 +335,7 @@ export function createSessionServer(
335
335
  const { web, assertOutboundAllowed, assertSendable, getAccess, resolveUserName, inboxDir, consumeAck } = deps
336
336
 
337
337
  const server = new Server(
338
- { name: 'slack-channel-router', version: '0.1.0' },
338
+ { name: MCP_SERVER_NAME, version: '0.1.0' },
339
339
  {
340
340
  capabilities: {
341
341
  experimental: { 'claude/channel': {} },
package/src/restart.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  export interface RestartDeps {
15
15
  isSessionAlive(channelId: string): Promise<boolean>
16
16
  killSession(channelId: string): Promise<void>
17
- launchSession(channelId: string, cwd: string): Promise<boolean>
17
+ launchSession(channelId: string, cwd: string, sessionId?: string): Promise<boolean>
18
18
  getRestartDelay(): number
19
19
  isShuttingDown(): boolean
20
20
  }
@@ -46,7 +46,7 @@ export function initRestart(d: RestartDeps): void {
46
46
  // scheduleRestart
47
47
  // ---------------------------------------------------------------------------
48
48
 
49
- export function scheduleRestart(channelId: string, cwd: string): void {
49
+ export function scheduleRestart(channelId: string, cwd: string, sessionId?: string): void {
50
50
  if (!deps) {
51
51
  console.error('[slack] scheduleRestart: deps not initialized — skipping')
52
52
  return
@@ -109,7 +109,7 @@ export function scheduleRestart(channelId: string, cwd: string): void {
109
109
 
110
110
  let ok: boolean
111
111
  try {
112
- ok = await deps.launchSession(channelId, cwd)
112
+ ok = await deps.launchSession(channelId, cwd, sessionId)
113
113
  } catch (err) {
114
114
  console.error(`[slack] restart: launchSession threw for channel=${channelId}:`, err)
115
115
  ok = false
package/src/server.ts CHANGED
@@ -37,7 +37,7 @@ import {
37
37
  type Access,
38
38
  type GateResult,
39
39
  } from './lib.ts'
40
- import { loadConfig, expandTilde, type RoutingConfig } from './config.ts'
40
+ import { loadConfig, expandTilde, type RoutingConfig, MCP_SERVER_NAME } from './config.ts'
41
41
  import { readSessions, writeSessions } from './sessions.ts'
42
42
  import { defaultTmuxClient, sessionName, isClaudeRunning } from './tmux.ts'
43
43
  import { startupSessionManager, launchSession } from './session-manager.ts'
@@ -296,7 +296,7 @@ function initPendingSession(): { pendingId: string; transport: WebStandardStream
296
296
  : undefined
297
297
  if (channelId) {
298
298
  console.error(`[slack] Session disconnected: channel=${channelId} cwd="${cwd}"`)
299
- scheduleRestart(channelId, cwd)
299
+ scheduleRestart(channelId, cwd, readSessions()[channelId]?.sessionId)
300
300
  } else {
301
301
  console.error(`[slack] Session disconnected: cwd="${cwd}"`)
302
302
  }
@@ -1266,7 +1266,7 @@ export async function main(): Promise<void> {
1266
1266
  : undefined
1267
1267
  if (channelId) {
1268
1268
  console.error(`[slack] Session disconnected (SSE abort): channel=${channelId} cwd="${cwd}"`)
1269
- scheduleRestart(channelId, cwd)
1269
+ scheduleRestart(channelId, cwd, readSessions()[channelId]?.sessionId)
1270
1270
  } else {
1271
1271
  console.error(`[slack] Session disconnected (SSE abort): cwd="${cwd}"`)
1272
1272
  }
@@ -1288,10 +1288,10 @@ export async function main(): Promise<void> {
1288
1288
  console.error(`[slack] MCP server listening on http://${mcpHost}:${mcpPort}/mcp`)
1289
1289
  console.error('')
1290
1290
  console.error('Save this to ~/.claude/slack-mcp.json:')
1291
- console.error(JSON.stringify({ mcpServers: { 'slack-channel-router': { type: 'http', url: `http://${mcpHost}:${mcpPort}/mcp` } } }, null, 2))
1291
+ console.error(JSON.stringify({ mcpServers: { [MCP_SERVER_NAME]: { type: 'http', url: `http://${mcpHost}:${mcpPort}/mcp` } } }, null, 2))
1292
1292
  console.error('')
1293
1293
  console.error('Then launch Claude from a project directory with:')
1294
- console.error(' claude --mcp-config ~/.claude/slack-mcp.json --dangerously-load-development-channels server:slack-channel-router')
1294
+ console.error(` claude --mcp-config ~/.claude/slack-mcp.json --dangerously-load-development-channels server:${MCP_SERVER_NAME}`)
1295
1295
  console.error('')
1296
1296
 
1297
1297
  // Initialize restart module with adapters bridging tmux + session-manager
@@ -1311,9 +1311,13 @@ export async function main(): Promise<void> {
1311
1311
  const exists = await defaultTmuxClient.hasSession(name)
1312
1312
  if (exists) await defaultTmuxClient.killSession(name)
1313
1313
  },
1314
- launchSession: (channelId, cwd) => {
1314
+ launchSession: (channelId, cwd, sessionId) => {
1315
1315
  if (!routingConfig) return Promise.resolve(false)
1316
- return launchSession(channelId, cwd, routingConfig, defaultTmuxClient, readSessions, writeSessions)
1316
+ const resolvedSessionId = sessionId ?? readSessions()[channelId]?.sessionId
1317
+ return launchSession(
1318
+ channelId, cwd, routingConfig, defaultTmuxClient, readSessions, writeSessions,
1319
+ resolvedSessionId !== undefined ? { sessionId: resolvedSessionId } : undefined,
1320
+ )
1317
1321
  },
1318
1322
  getRestartDelay: () => routingConfig?.session_restart_delay ?? 60,
1319
1323
  isShuttingDown: () => shuttingDown,
@@ -1,16 +1,20 @@
1
1
  /**
2
2
  * session-manager.ts — Startup orchestration for tmux-managed Claude Code sessions.
3
3
  *
4
- * Handles two cases per route at server startup:
5
- * exists — tmux session foundkill and relaunch
6
- * missing no tmux session → launch fresh
4
+ * Handles three cases per route at server startup:
5
+ * reconnect — tmux session exists AND Claude is running send /mcp reconnect, do not relaunch
6
+ * resume — dead or missing process with stored session ID kill stale session, relaunch with --resume
7
+ * fresh — dead or missing process without stored session ID → kill stale session, launch fresh
7
8
  *
8
9
  * SPDX-License-Identifier: MIT
9
10
  */
10
11
 
12
+ import { readdirSync, readFileSync } from 'fs'
13
+ import { join } from 'path'
14
+ import { homedir } from 'os'
11
15
  import { type TmuxClient, sessionName, isClaudeRunning } from './tmux.ts'
12
16
  import { type SessionsMap } from './sessions.ts'
13
- import { type RoutingConfig } from './config.ts'
17
+ import { type RoutingConfig, MCP_SERVER_NAME } from './config.ts'
14
18
 
15
19
  // ---------------------------------------------------------------------------
16
20
  // Types
@@ -19,14 +23,63 @@ import { type RoutingConfig } from './config.ts'
19
23
  export interface LaunchOptions {
20
24
  /** Maximum time in ms to poll for the safety prompt. Default: 60000. */
21
25
  pollTimeout?: number
26
+ /** Claude session UUID to resume. When provided, --resume <id> is appended to the CLI command. */
27
+ sessionId?: string
22
28
  }
23
29
 
24
30
  export interface SessionStateResult {
25
31
  channelId: string
26
- action: 'relaunched' | 'launched' | 'failed'
32
+ action: 'reconnected' | 'launched' | 'resumed' | 'failed'
27
33
  sessionName: string
28
34
  }
29
35
 
36
+ // ---------------------------------------------------------------------------
37
+ // Session ID capture helper
38
+ // ---------------------------------------------------------------------------
39
+
40
+ /**
41
+ * Polls ~/.claude/sessions/ for a new session file matching the given CWD
42
+ * with a startedAt timestamp after `launchTimestamp`. Returns the sessionId
43
+ * string if found, or undefined if capture fails or times out.
44
+ *
45
+ * Polls up to ~2 seconds (4 × 500ms intervals).
46
+ */
47
+ async function captureSessionId(cwd: string, launchTimestamp: number): Promise<string | undefined> {
48
+ const sessionsDir = join(homedir(), '.claude', 'sessions')
49
+ const POLL_INTERVAL_MS = 500
50
+ const POLL_ATTEMPTS = 4
51
+
52
+ for (let i = 0; i < POLL_ATTEMPTS; i++) {
53
+ await new Promise<void>((resolve) => setTimeout(resolve, POLL_INTERVAL_MS))
54
+ try {
55
+ const files = readdirSync(sessionsDir)
56
+ for (const file of files) {
57
+ if (!file.endsWith('.json')) continue
58
+ try {
59
+ const raw = readFileSync(join(sessionsDir, file), 'utf-8')
60
+ const entry = JSON.parse(raw)
61
+ if (
62
+ typeof entry === 'object' &&
63
+ entry !== null &&
64
+ entry.cwd === cwd &&
65
+ typeof entry.startedAt === 'number' &&
66
+ entry.startedAt > launchTimestamp &&
67
+ typeof entry.sessionId === 'string' &&
68
+ entry.sessionId.length > 0
69
+ ) {
70
+ return entry.sessionId as string
71
+ }
72
+ } catch {
73
+ // skip unreadable or malformed files
74
+ }
75
+ }
76
+ } catch {
77
+ // sessionsDir may not exist yet; keep polling
78
+ }
79
+ }
80
+ return undefined
81
+ }
82
+
30
83
  // ---------------------------------------------------------------------------
31
84
  // launchSession
32
85
  // ---------------------------------------------------------------------------
@@ -36,6 +89,10 @@ export interface SessionStateResult {
36
89
  * correct MCP config, polls for the safety prompt, and records the session
37
90
  * in sessions.json on success.
38
91
  *
92
+ * When options.sessionId is provided, appends --resume <id> to the CLI
93
+ * command. If the resume attempt fails, kills the tmux session and retries
94
+ * once with a fresh launch (no --resume).
95
+ *
39
96
  * Returns true on success, false on failure.
40
97
  */
41
98
  export async function launchSession(
@@ -49,64 +106,114 @@ export async function launchSession(
49
106
  ): Promise<boolean> {
50
107
  const name = sessionName(cwd)
51
108
  const pollTimeout = options?.pollTimeout ?? 60_000
109
+ const resumeSessionId = options?.sessionId
52
110
 
53
- // Create detached tmux session with the channel's CWD
54
- await tmuxClient.newSession(name, cwd)
55
- console.error(`[slack] Session created: ${name} (cwd="${cwd}")`)
56
-
57
- // Send the claude launch command, then Enter to execute it
58
111
  const escapedConfigPath = routingConfig.mcp_config_path.replace(/'/g, "'\\''")
59
- const launchCmd = `claude --mcp-config '${escapedConfigPath}' --dangerously-load-development-channels server:slack-channel-router`
60
- await tmuxClient.sendKeys(name, launchCmd)
61
- await tmuxClient.sendKeys(name, 'Enter')
62
- console.error(`[slack] Claude launch command sent to session: ${name}`)
112
+ const baseCmd = `claude --mcp-config '${escapedConfigPath}' --dangerously-load-development-channels server:${MCP_SERVER_NAME}`
63
113
 
64
- // Poll capturePane for the safety prompt with exponential backoff.
65
- // Start at 500ms, double each iteration, cap at 5s, total limit 60s.
66
114
  const POLL_START_MS = 500
67
115
  const POLL_CAP_MS = 5_000
68
116
  const PROMPT_TEXT = 'I am using this for local development'
69
117
 
70
- let delay = POLL_START_MS
71
- const deadline = Date.now() + pollTimeout
118
+ // Inner helper: sends the launch command and polls for the safety prompt.
119
+ // Returns { ok: true, capturedId } on success (capturedId may be undefined if capture failed),
120
+ // or { ok: false } when Claude is not running after the poll timeout.
121
+ async function attemptLaunch(
122
+ withResumeId: string | undefined,
123
+ ): Promise<{ ok: true; capturedId: string | undefined } | { ok: false }> {
124
+ const safeResumeId = withResumeId && /^[a-zA-Z0-9_-]+$/.test(withResumeId) ? withResumeId : undefined
125
+ if (withResumeId && !safeResumeId) {
126
+ console.error(`[slack] Invalid session ID format — ignoring resume for channel=${channelId}`)
127
+ }
128
+ const launchTimestamp = Date.now()
129
+ const launchCmd = safeResumeId ? `${baseCmd} --resume ${safeResumeId}` : baseCmd
130
+ if (safeResumeId) {
131
+ console.error(`[slack] Attempting resume launch for channel=${channelId} sessionId=${safeResumeId}`)
132
+ } else {
133
+ console.error(`[slack] Attempting fresh launch for channel=${channelId}`)
134
+ }
135
+ await tmuxClient.sendKeys(name, launchCmd)
136
+ await tmuxClient.sendKeys(name, 'Enter')
137
+ console.error(`[slack] Claude launch command sent to session: ${name}`)
72
138
 
73
- while (Date.now() < deadline) {
74
- await new Promise<void>((resolve) => setTimeout(resolve, delay))
75
- delay = Math.min(delay * 2, POLL_CAP_MS)
139
+ let delay = POLL_START_MS
140
+ const deadline = launchTimestamp + pollTimeout
76
141
 
77
- let pane: string
78
- try {
79
- pane = await tmuxClient.capturePane(name)
80
- } catch {
81
- // capturePane failure is terminal — session may have died
82
- break
142
+ while (Date.now() < deadline) {
143
+ await new Promise<void>((resolve) => setTimeout(resolve, delay))
144
+ delay = Math.min(delay * 2, POLL_CAP_MS)
145
+
146
+ let pane: string
147
+ try {
148
+ pane = await tmuxClient.capturePane(name)
149
+ } catch {
150
+ // capturePane failure is terminal — session may have died
151
+ break
152
+ }
153
+
154
+ if (pane.includes(PROMPT_TEXT)) {
155
+ await tmuxClient.sendKeys(name, 'Enter')
156
+ console.error(`[slack] Safety prompt acknowledged in session: ${name}`)
157
+ const capturedId = await captureSessionId(cwd, launchTimestamp)
158
+ if (capturedId) {
159
+ console.error(`[slack] Session ID captured for channel=${channelId}: ${capturedId}`)
160
+ } else {
161
+ console.error(`[slack] Session ID capture failed for channel=${channelId} — continuing without it`)
162
+ }
163
+ return { ok: true, capturedId }
164
+ }
83
165
  }
84
166
 
85
- if (pane.includes(PROMPT_TEXT)) {
86
- // Safety prompt found acknowledge it and record success
87
- await tmuxClient.sendKeys(name, 'Enter')
88
- console.error(`[slack] Safety prompt acknowledged in session: ${name}`)
89
- const sessions = readSessionsFn()
90
- sessions[channelId] = { tmuxSession: name, lastLaunch: new Date().toISOString() }
91
- writeSessionsFn(sessions)
92
- console.error(`[slack] Session recorded in sessions.json: channel=${channelId}`)
93
- return true
167
+ // Prompt not found — check if Claude is running anyway (forward-compatible)
168
+ const running = await isClaudeRunning(name, tmuxClient)
169
+ if (running) {
170
+ console.error(`[slack] Safety prompt not found but Claude is running — accepting session: ${name}`)
171
+ const capturedId = await captureSessionId(cwd, launchTimestamp)
172
+ if (capturedId) {
173
+ console.error(`[slack] Session ID captured for channel=${channelId}: ${capturedId}`)
174
+ } else {
175
+ console.error(`[slack] Session ID capture failed for channel=${channelId} — continuing without it`)
176
+ }
177
+ return { ok: true, capturedId }
94
178
  }
179
+
180
+ return { ok: false }
95
181
  }
96
182
 
97
- // Prompt not found check if Claude is running anyway (forward-compatible)
98
- const running = await isClaudeRunning(name, tmuxClient)
99
- if (running) {
100
- console.error(`[slack] Safety prompt not found but Claude is running — accepting session: ${name}`)
101
- const sessions = readSessionsFn()
102
- sessions[channelId] = { tmuxSession: name, lastLaunch: new Date().toISOString() }
103
- writeSessionsFn(sessions)
104
- console.error(`[slack] Session recorded in sessions.json: channel=${channelId}`)
105
- return true
183
+ // Create detached tmux session with the channel's CWD
184
+ await tmuxClient.newSession(name, cwd)
185
+ console.error(`[slack] Session created: ${name} (cwd="${cwd}")`)
186
+
187
+ // Attempt launch (with --resume if sessionId provided)
188
+ let result = await attemptLaunch(resumeSessionId)
189
+
190
+ // resumeSessionId was provided but launch failed — fall back to a fresh launch
191
+ if (!result.ok && resumeSessionId !== undefined) {
192
+ console.error(`[slack] Resume failed for channel=${channelId} — killing session and retrying with fresh launch`)
193
+ try {
194
+ await tmuxClient.killSession(name)
195
+ } catch {
196
+ // ignore kill errors; proceed with fresh session creation
197
+ }
198
+ await tmuxClient.newSession(name, cwd)
199
+ console.error(`[slack] Session recreated for fresh fallback: ${name} (cwd="${cwd}")`)
200
+ result = await attemptLaunch(undefined)
106
201
  }
107
202
 
108
- console.error(`[slack] Session launch failed — Claude not running in session: ${name}`)
109
- return false
203
+ if (!result.ok) {
204
+ console.error(`[slack] Session launch failed — Claude not running in session: ${name}`)
205
+ return false
206
+ }
207
+
208
+ const sessions = readSessionsFn()
209
+ sessions[channelId] = {
210
+ tmuxSession: name,
211
+ lastLaunch: new Date().toISOString(),
212
+ ...(result.capturedId !== undefined ? { sessionId: result.capturedId } : {}),
213
+ }
214
+ writeSessionsFn(sessions)
215
+ console.error(`[slack] Session recorded in sessions.json: channel=${channelId}`)
216
+ return true
110
217
  }
111
218
 
112
219
  // ---------------------------------------------------------------------------
@@ -114,9 +221,11 @@ export async function launchSession(
114
221
  // ---------------------------------------------------------------------------
115
222
 
116
223
  /**
117
- * On server startup, inspects all configured routes and takes action:
118
- * - Exists (session found): kills session and relaunches
119
- * - Missing: launches fresh
224
+ * On server startup, inspects all configured routes and takes action using a
225
+ * three-branch decision tree per route:
226
+ * - Reconnect: tmux session exists AND Claude is running → send /mcp reconnect, do not relaunch
227
+ * - Resume: dead or missing process with stored session ID → kill stale session, relaunch with --resume
228
+ * - Fresh: dead or missing process without stored session ID → kill stale session, launch fresh
120
229
  *
121
230
  * Returns early with a warning if tmux is unavailable.
122
231
  */
@@ -138,6 +247,9 @@ export async function startupSessionManager(
138
247
 
139
248
  const results: SessionStateResult[] = []
140
249
 
250
+ // Load stored session IDs for all channels once before the route iteration loop
251
+ const storedSessions = readSessionsFn()
252
+
141
253
  for (const [channelId, route] of Object.entries(routingConfig.routes)) {
142
254
  const name = sessionName(route.cwd)
143
255
 
@@ -145,17 +257,38 @@ export async function startupSessionManager(
145
257
  const exists = await tmuxClient.hasSession(name)
146
258
 
147
259
  if (exists) {
148
- // Session exists kill and relaunch regardless of whether Claude is running
149
- console.error(`[slack] Session exists — killing and relaunching: channel=${channelId} session=${name}`)
260
+ const running = await isClaudeRunning(name, tmuxClient)
261
+
262
+ if (running) {
263
+ // Branch 1: Reconnect — session live, send /mcp reconnect <server-name>
264
+ console.error(`[slack] Session live — reconnecting MCP server "${MCP_SERVER_NAME}": channel=${channelId} session=${name}`)
265
+ await tmuxClient.sendKeys(name, `/mcp reconnect ${MCP_SERVER_NAME}`)
266
+ await tmuxClient.sendKeys(name, 'Enter')
267
+ results.push({ channelId, action: 'reconnected', sessionName: name })
268
+ continue
269
+ }
270
+ }
271
+
272
+ // Branch 2 or 3: Dead or missing process — check for stored session ID
273
+ const storedSessionId = storedSessions[channelId]?.sessionId
274
+
275
+ if (exists) {
276
+ // Kill stale tmux session before relaunching
277
+ console.error(`[slack] Stale session found — killing before relaunch: channel=${channelId} session=${name}`)
150
278
  await tmuxClient.killSession(name)
279
+ }
280
+
281
+ if (storedSessionId) {
282
+ // Branch 2: Resume — launch with stored session ID
283
+ console.error(`[slack] Dead/missing process with stored session ID — resuming: channel=${channelId} session=${name} sessionId=${storedSessionId}`)
151
284
  const ok = await launchSession(
152
285
  channelId, route.cwd, routingConfig, tmuxClient,
153
- readSessionsFn, writeSessionsFn, options,
286
+ readSessionsFn, writeSessionsFn, { ...options, sessionId: storedSessionId },
154
287
  )
155
- results.push({ channelId, action: ok ? 'relaunched' : 'failed', sessionName: name })
288
+ results.push({ channelId, action: ok ? 'resumed' : 'failed', sessionName: name })
156
289
  } else {
157
- // No session — launch fresh
158
- console.error(`[slack] No session found — launching: channel=${channelId} session=${name}`)
290
+ // Branch 3: Fresh — launch without session ID
291
+ console.error(`[slack] No stored session ID — launching fresh: channel=${channelId} session=${name}`)
159
292
  const ok = await launchSession(
160
293
  channelId, route.cwd, routingConfig, tmuxClient,
161
294
  readSessionsFn, writeSessionsFn, options,
package/src/sessions.ts CHANGED
@@ -23,6 +23,7 @@ import { expandTilde } from './config.ts'
23
23
  export interface SessionRecord {
24
24
  tmuxSession: string
25
25
  lastLaunch: string // ISO-8601, e.g. new Date().toISOString()
26
+ sessionId?: string // Claude session UUID, captured after launch
26
27
  }
27
28
 
28
29
  export type SessionsMap = Record<string, SessionRecord>