claude-threads 1.21.0 → 1.21.1

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/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.21.1] - 2026-08-07
9
+
10
+ ### Fixed
11
+ - **A respawning session can no longer be torn down by its own dying Claude process.** Two stacked races made `!cd` / `!permissions` / worktree respawns flaky (seen as the recurring `!cd should restart Claude CLI` failure on main's Integration Tests): a last event flushed by the dying process ran `resetSessionActivity`, which unconditionally flipped the session's `restarting` state back to `active` — so when the old process's exit landed, `handleExit` mistook it for the current process dying and did a full session teardown mid-restart. `resetSessionActivity` now leaves `restarting`/`cancelling` states alone, `handleExit` ignores exits from a CLI instance that is no longer the session's current one (the exit event can be delivered after the respawn already swapped in the new instance), and a successful respawn transitions to `active` explicitly instead of relying on the old exit's side effect. `ClaudeCli.kill()` gained an integration-test caller trace, mirroring the existing `sendMessage` one — attributing kills is the key question when debugging these races in CI logs.
12
+ - **`checkGitHubCli` unit tests no longer spawn the real `gh` CLI.** The two subprocess probes each carry a 5s timeout, overrunning bun's 5s per-test budget on a slow runner (the flaky `checkGitHubCli` timeout on main's CI). The exec call is now injectable and the tests cover all three outcomes (installed+authenticated, not installed, not authenticated) deterministically.
13
+ - **Integration: the `!cd` confirmation wait no longer matches the user's own message.** The loose `/changed|directory|\/tmp/i` pattern matched the `!cd /tmp` command itself, ending the test while the respawn was still in flight — `afterEach`'s `killAllSessions` then raced the restart and leaked an orphaned mock CLI into the next test.
14
+
8
15
  ## [1.21.0] - 2026-08-07
9
16
 
10
17
  ### Fixed
package/dist/index.js CHANGED
@@ -56341,6 +56341,12 @@ class ClaudeCli extends EventEmitter2 {
56341
56341
  const pid = proc.pid;
56342
56342
  this.process = null;
56343
56343
  this.log.debug(`Killing Claude process (pid=${pid})`);
56344
+ if (process.env.INTEGRATION_TEST === "1") {
56345
+ const stack = new Error().stack?.split(`
56346
+ `).slice(2, 7).join(" > ").replace(/\s+at\s+/g, " < ") ?? "?";
56347
+ process.stderr.write(`[claude-cli kill pid=${pid}] | ${stack}
56348
+ `);
56349
+ }
56344
56350
  return new Promise((resolve4) => {
56345
56351
  this.log.debug("Sending first SIGINT");
56346
56352
  proc.kill("SIGINT");
@@ -58526,7 +58532,10 @@ function resetSessionActivity(session) {
58526
58532
  session.lastActivityAt = new Date;
58527
58533
  session.timeoutWarningPosted = false;
58528
58534
  session.lifecyclePostId = undefined;
58529
- transitionTo(session, "active");
58535
+ const state = session.lifecycle.state;
58536
+ if (state !== "restarting" && state !== "cancelling") {
58537
+ transitionTo(session, "active");
58538
+ }
58530
58539
  if (session.worktreeInfo?.worktreePath) {
58531
58540
  updateWorktreeActivity(session.worktreeInfo.worktreePath, session.sessionId);
58532
58541
  }
@@ -62641,9 +62650,9 @@ _Reported via claude-threads bug report feature_`);
62641
62650
  function escapeShell(str2) {
62642
62651
  return str2.replace(/"/g, "\\\"");
62643
62652
  }
62644
- function checkGitHubCli() {
62653
+ function checkGitHubCli(exec2 = execSync2) {
62645
62654
  try {
62646
- execSync2("gh --version", {
62655
+ exec2("gh --version", {
62647
62656
  encoding: "utf-8",
62648
62657
  timeout: 5000,
62649
62658
  stdio: ["pipe", "pipe", "pipe"]
@@ -62656,7 +62665,7 @@ function checkGitHubCli() {
62656
62665
  };
62657
62666
  }
62658
62667
  try {
62659
- execSync2("gh auth status", {
62668
+ exec2("gh auth status", {
62660
62669
  encoding: "utf-8",
62661
62670
  timeout: 5000,
62662
62671
  stdio: ["pipe", "pipe", "pipe"]
@@ -68233,10 +68242,14 @@ async function createAndSwitchToWorktree(session, branch, username, options) {
68233
68242
  appendSystemPrompt: await buildAppendSystemPrompt(session.platform, session.platformId, existing.path, session.threadId, session.startedBy, session.sessionAllowedUsers, options.appendSystemPrompt ?? "", options.githubEmailsStore, { omitSessionContext: !needsTitlePrompt, userAttribution: session.userAttribution })
68234
68243
  };
68235
68244
  session.messageManager?.clearClaudeSessionState();
68236
- session.claude = new ClaudeCli(cliOptions);
68237
- session.claude.on("event", (e) => options.handleEvent(session.sessionId, e));
68238
- session.claude.on("exit", (code) => options.handleExit(session.sessionId, code));
68239
- session.claude.start();
68245
+ const newClaude = new ClaudeCli(cliOptions);
68246
+ session.claude = newClaude;
68247
+ newClaude.on("event", (e) => options.handleEvent(session.sessionId, e));
68248
+ newClaude.on("exit", (code) => options.handleExit(session.sessionId, code, newClaude));
68249
+ newClaude.start();
68250
+ if (isSessionRestarting(session)) {
68251
+ transitionTo(session, "active");
68252
+ }
68240
68253
  }
68241
68254
  await options.updateSessionHeader(session);
68242
68255
  await post(session, "success", `${fmt.formatBold("Joined existing worktree")} for branch ${fmt.formatCode(branch)}
@@ -68326,10 +68339,14 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
68326
68339
  appendSystemPrompt: await buildAppendSystemPrompt(session.platform, session.platformId, worktreePath, session.threadId, session.startedBy, session.sessionAllowedUsers, options.appendSystemPrompt ?? "", options.githubEmailsStore, { omitSessionContext: !needsTitlePrompt, userAttribution: session.userAttribution })
68327
68340
  };
68328
68341
  session.messageManager?.clearClaudeSessionState();
68329
- session.claude = new ClaudeCli(cliOptions);
68330
- session.claude.on("event", (e) => options.handleEvent(session.sessionId, e));
68331
- session.claude.on("exit", (code) => options.handleExit(session.sessionId, code));
68332
- session.claude.start();
68342
+ const newClaude = new ClaudeCli(cliOptions);
68343
+ session.claude = newClaude;
68344
+ newClaude.on("event", (e) => options.handleEvent(session.sessionId, e));
68345
+ newClaude.on("exit", (code) => options.handleExit(session.sessionId, code, newClaude));
68346
+ newClaude.start();
68347
+ if (isSessionRestarting(session)) {
68348
+ transitionTo(session, "active");
68349
+ }
68333
68350
  }
68334
68351
  await options.updateSessionHeader(session);
68335
68352
  const shortWorktreePath = shortenPath(worktreePath, undefined, { path: worktreePath, branch });
@@ -69230,15 +69247,21 @@ async function restartClaudeSession(session, cliOptions, ctx, actionName) {
69230
69247
  if (!cliOptions.resume) {
69231
69248
  session.messageManager?.clearClaudeSessionState();
69232
69249
  }
69233
- session.claude = new ClaudeCli(cliOptions);
69234
- session.claude.on("event", (e) => ctx.ops.handleEvent(session.sessionId, e));
69235
- session.claude.on("exit", (code) => ctx.ops.handleExit(session.sessionId, code));
69236
- session.claude.on("rate-limit", (hit) => handleRateLimit(session, hit, ctx));
69250
+ const newClaude = new ClaudeCli(cliOptions);
69251
+ session.claude = newClaude;
69252
+ newClaude.on("event", (e) => ctx.ops.handleEvent(session.sessionId, e));
69253
+ newClaude.on("exit", (code) => ctx.ops.handleExit(session.sessionId, code, newClaude));
69254
+ newClaude.on("rate-limit", (hit) => handleRateLimit(session, hit, ctx));
69237
69255
  try {
69238
- session.claude.start();
69256
+ newClaude.start();
69257
+ if (isSessionRestarting(session)) {
69258
+ transitionTo(session, "active");
69259
+ }
69239
69260
  return true;
69240
69261
  } catch (err) {
69241
- transitionTo(session, "active");
69262
+ if (isSessionRestarting(session)) {
69263
+ transitionTo(session, "active");
69264
+ }
69242
69265
  await logAndNotify(err, { action: actionName, session });
69243
69266
  return false;
69244
69267
  }
@@ -70351,7 +70374,7 @@ async function startSession(options, username, displayName, replyToPostId, platf
70351
70374
  await ctx.ops.updateStickyMessage();
70352
70375
  ctx.ops.startTyping(session);
70353
70376
  claude.on("event", (e) => ctx.ops.handleEvent(sessionId, e));
70354
- claude.on("exit", (code) => ctx.ops.handleExit(sessionId, code));
70377
+ claude.on("exit", (code) => ctx.ops.handleExit(sessionId, code, claude));
70355
70378
  claude.on("rate-limit", (hit) => handleRateLimit(session, hit, ctx));
70356
70379
  try {
70357
70380
  claude.start();
@@ -70546,7 +70569,7 @@ Please start a new session.`), { action: "Post resume failure notification" });
70546
70569
  ctx.ops.emitSessionAdd(session);
70547
70570
  keepAlive.sessionStarted();
70548
70571
  claude.on("event", (e) => ctx.ops.handleEvent(sessionId, e));
70549
- claude.on("exit", (code) => ctx.ops.handleExit(sessionId, code));
70572
+ claude.on("exit", (code) => ctx.ops.handleExit(sessionId, code, claude));
70550
70573
  claude.on("rate-limit", (hit) => handleRateLimit(session, hit, ctx));
70551
70574
  try {
70552
70575
  claude.start();
@@ -70634,7 +70657,7 @@ async function resumePausedSession(threadId, message, files, ctx, username) {
70634
70657
  log31.warn(`Failed to resume session ${shortId}..., could not send message`);
70635
70658
  }
70636
70659
  }
70637
- async function handleExit(sessionId, code, ctx) {
70660
+ async function handleExit(sessionId, code, ctx, source) {
70638
70661
  const session = mutableSessions(ctx).get(sessionId);
70639
70662
  const shortId = sessionId.substring(0, 8);
70640
70663
  sessionLog6(session).debug(`handleExit called code=${code} isShuttingDown=${ctx.state.isShuttingDown}`);
@@ -70642,6 +70665,10 @@ async function handleExit(sessionId, code, ctx) {
70642
70665
  log31.debug(`Session ${shortId}... not found (already cleaned up)`);
70643
70666
  return;
70644
70667
  }
70668
+ if (source && session.claude !== source) {
70669
+ sessionLog6(session).debug(`Ignoring exit from replaced Claude process`);
70670
+ return;
70671
+ }
70645
70672
  if (isSessionRestarting(session)) {
70646
70673
  sessionLog6(session).debug(`Restarting, skipping cleanup`);
70647
70674
  transitionTo(session, "active");
@@ -71365,7 +71392,7 @@ class SessionManager extends EventEmitter4 {
71365
71392
  updateSessionHeader: (s) => this.updateSessionHeader(s),
71366
71393
  updateStickyMessage: () => this.updateStickyMessage(),
71367
71394
  handleEvent: (sid, e) => this.handleEvent(sid, e),
71368
- handleExit: (sid, code) => this.handleExit(sid, code),
71395
+ handleExit: (sid, code, source) => this.handleExit(sid, code, source),
71369
71396
  killSession: (tid) => this.killSession(tid),
71370
71397
  shouldPromptForWorktree: (s) => this.shouldPromptForWorktree(s),
71371
71398
  postWorktreePrompt: (s, r) => this.postWorktreePrompt(s, r),
@@ -71469,8 +71496,8 @@ class SessionManager extends EventEmitter4 {
71469
71496
  session.messageManager.handleEvent(event);
71470
71497
  handleEventPostProcessing(session, event, this.getContext());
71471
71498
  }
71472
- async handleExit(sessionId, code) {
71473
- await handleExit(sessionId, code, this.getContext());
71499
+ async handleExit(sessionId, code, source) {
71500
+ await handleExit(sessionId, code, this.getContext(), source);
71474
71501
  }
71475
71502
  startTyping(session) {
71476
71503
  const wasTyping = session.timers.typingTimer !== null;
@@ -71985,7 +72012,7 @@ class SessionManager extends EventEmitter4 {
71985
72012
  worktreeMode: this.worktreeMode,
71986
72013
  permissionTimeoutMs: this.limits.permissionTimeoutSeconds * 1000,
71987
72014
  handleEvent: (tid, e) => this.handleEvent(tid, e),
71988
- handleExit: (tid, code) => this.handleExit(tid, code),
72015
+ handleExit: (tid, code, source) => this.handleExit(tid, code, source),
71989
72016
  updateSessionHeader: (s) => this.updateSessionHeader(s),
71990
72017
  flush: async (s) => {
71991
72018
  if (s.messageManager) {
@@ -56263,6 +56263,12 @@ class ClaudeCli extends EventEmitter2 {
56263
56263
  const pid = proc.pid;
56264
56264
  this.process = null;
56265
56265
  this.log.debug(`Killing Claude process (pid=${pid})`);
56266
+ if (process.env.INTEGRATION_TEST === "1") {
56267
+ const stack = new Error().stack?.split(`
56268
+ `).slice(2, 7).join(" > ").replace(/\s+at\s+/g, " < ") ?? "?";
56269
+ process.stderr.write(`[claude-cli kill pid=${pid}] | ${stack}
56270
+ `);
56271
+ }
56266
56272
  return new Promise((resolve5) => {
56267
56273
  this.log.debug("Sending first SIGINT");
56268
56274
  proc.kill("SIGINT");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.21.0",
3
+ "version": "1.21.1",
4
4
  "description": "Run Claude Code from Slack or Mattermost. Sessions stream live into threads where your whole team can watch and steer.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",