claude-threads 1.9.4 → 1.10.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/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ 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.10.0] - 2026-04-29
9
+
10
+ ### Added
11
+ - **Thread permalink in Claude session context.** The system-prompt context block now includes `**Thread:** <permalink>` alongside the existing platform and working-directory entries. When Claude opens a PR, files a ticket, or otherwise produces an artifact for someone to review later, it can paste the link back into the description so reviewers can trace the change to the discussion it came from. Mattermost and Slack both already exposed `getThreadLink()`; pure plumbing change. Also restores the platform/working-directory context on the worktree-restart path, which had been silently dropped pre-existing. (#352)
12
+
13
+ ### Fixed
14
+ - **Sticky-by-thread Claude account binding (multi-account mode).** In multi-account configurations the `claudeAccountId` written to `sessions.json` and the `$HOME` Claude was actually spawned under could drift apart under concurrent acquisitions, leaving sessions unresumable after a bot restart with `Detected permanent failure: The conversation history for this session no longer exists`. Real-world incident: 14 sessions soft-deleted in one restart cycle, 3 of them genuine victims of this drift. `AccountPool.acquire()` now binds threads to accounts deterministically via `accounts[hash(threadId) % n]` (FNV-1a, dependency-free), so the spawn-time `$HOME` and the persisted id both derive from the same hash and cannot drift. `preferredId` still wins over the sticky binding (resume invariant: OAuth history can't move), and the sticky path falls through to round-robin when the chosen account is in cooldown. (#350)
15
+
8
16
  ## [1.9.4] - 2026-04-29
9
17
 
10
18
  ### Fixed
package/dist/index.js CHANGED
@@ -53655,6 +53655,14 @@ class SessionStore {
53655
53655
  // src/claude/account-pool.ts
53656
53656
  init_logger();
53657
53657
  var log5 = createLogger("account-pool");
53658
+ function hashThreadId(threadId) {
53659
+ let h = 2166136261;
53660
+ for (let i2 = 0;i2 < threadId.length; i2++) {
53661
+ h ^= threadId.charCodeAt(i2);
53662
+ h = Math.imul(h, 16777619);
53663
+ }
53664
+ return h >>> 0;
53665
+ }
53658
53666
 
53659
53667
  class AccountPool {
53660
53668
  accounts;
@@ -53686,7 +53694,7 @@ class AccountPool {
53686
53694
  get size() {
53687
53695
  return this.accounts.length;
53688
53696
  }
53689
- acquire(preferredId) {
53697
+ acquire(preferredId, threadId) {
53690
53698
  if (this.isEmpty)
53691
53699
  return null;
53692
53700
  if (preferredId) {
@@ -53699,6 +53707,14 @@ class AccountPool {
53699
53707
  }
53700
53708
  const now = Date.now();
53701
53709
  const n = this.accounts.length;
53710
+ if (threadId) {
53711
+ const sticky = this.accounts[hashThreadId(threadId) % n];
53712
+ const cooling = this.coolingUntil.get(sticky.id) ?? 0;
53713
+ if (cooling <= now) {
53714
+ this.incrementActive(sticky.id);
53715
+ return sticky;
53716
+ }
53717
+ }
53702
53718
  for (let i2 = 0;i2 < n; i2++) {
53703
53719
  const idx = (this.roundRobinIndex + i2) % n;
53704
53720
  const candidate = this.accounts[idx];
@@ -55582,9 +55598,10 @@ function formatClaudeCommand(cmd) {
55582
55598
  }
55583
55599
  return line;
55584
55600
  }
55585
- function buildSessionContext(platform, workingDir) {
55601
+ function buildSessionContext(platform, workingDir, threadId) {
55586
55602
  const platformName = platform.platformType.charAt(0).toUpperCase() + platform.platformType.slice(1);
55587
- return `**Platform:** ${platformName} (${platform.displayName}) | **Working Directory:** ${workingDir}`;
55603
+ const threadUrl = platform.getThreadLink(threadId);
55604
+ return `**Platform:** ${platformName} (${platform.displayName}) | **Working Directory:** ${workingDir} | **Thread:** ${threadUrl}`;
55588
55605
  }
55589
55606
  function generateChatPlatformPrompt() {
55590
55607
  const userCommands = COMMAND_REGISTRY.filter((cmd) => cmd.category !== "passthrough" && ["stop", "escape", "approve", "invite", "kick", "cd", "permissions", "update"].includes(cmd.command));
@@ -65912,7 +65929,7 @@ async function changeDirectory(session, newDir, username, ctx) {
65912
65929
  session.workingDir = absoluteDir;
65913
65930
  const newSessionId = randomUUID2();
65914
65931
  session.claudeSessionId = newSessionId;
65915
- const sessionContext = buildSessionContext(session.platform, absoluteDir);
65932
+ const sessionContext = buildSessionContext(session.platform, absoluteDir, session.threadId);
65916
65933
  const appendSystemPrompt = `${sessionContext}
65917
65934
 
65918
65935
  ${CHAT_PLATFORM_PROMPT}`;
@@ -66615,6 +66632,7 @@ async function createAndSwitchToWorktree(session, branch, username, options2) {
66615
66632
  const newSessionId = randomUUID3();
66616
66633
  session.claudeSessionId = newSessionId;
66617
66634
  const needsTitlePrompt = !session.sessionTitle;
66635
+ const sessionContext = needsTitlePrompt ? buildSessionContext(session.platform, existing.path, session.threadId) : null;
66618
66636
  const cliOptions = {
66619
66637
  workingDir: existing.path,
66620
66638
  threadId: session.threadId,
@@ -66627,7 +66645,9 @@ async function createAndSwitchToWorktree(session, branch, username, options2) {
66627
66645
  resume: false,
66628
66646
  chrome: options2.chromeEnabled,
66629
66647
  platformConfig: session.platform.getMcpConfig(),
66630
- appendSystemPrompt: needsTitlePrompt ? options2.appendSystemPrompt : undefined,
66648
+ appendSystemPrompt: sessionContext ? `${sessionContext}
66649
+
66650
+ ${options2.appendSystemPrompt}` : undefined,
66631
66651
  logSessionId: session.sessionId,
66632
66652
  permissionTimeoutMs: options2.permissionTimeoutMs
66633
66653
  };
@@ -66708,6 +66728,7 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
66708
66728
  const newSessionId = randomUUID3();
66709
66729
  session.claudeSessionId = newSessionId;
66710
66730
  const needsTitlePrompt = !session.sessionTitle;
66731
+ const sessionContext = needsTitlePrompt ? buildSessionContext(session.platform, worktreePath, session.threadId) : null;
66711
66732
  const cliOptions = {
66712
66733
  workingDir: worktreePath,
66713
66734
  threadId: session.threadId,
@@ -66720,7 +66741,9 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
66720
66741
  resume: false,
66721
66742
  chrome: options2.chromeEnabled,
66722
66743
  platformConfig: session.platform.getMcpConfig(),
66723
- appendSystemPrompt: needsTitlePrompt ? options2.appendSystemPrompt : undefined,
66744
+ appendSystemPrompt: sessionContext ? `${sessionContext}
66745
+
66746
+ ${options2.appendSystemPrompt}` : undefined,
66724
66747
  logSessionId: session.sessionId,
66725
66748
  permissionTimeoutMs: options2.permissionTimeoutMs
66726
66749
  };
@@ -67838,12 +67861,12 @@ async function startSession(options2, username, displayName, replyToPostId, plat
67838
67861
  permissionMode = "default";
67839
67862
  log26.info(`Starting session with interactive permissions (from !permissions command)`);
67840
67863
  }
67841
- const sessionContext = buildSessionContext(platform, workingDir);
67864
+ const sessionContext = buildSessionContext(platform, workingDir, actualThreadId);
67842
67865
  const systemPrompt = `${sessionContext}
67843
67866
 
67844
67867
  ${CHAT_PLATFORM_PROMPT}`;
67845
67868
  const platformMcpConfig = platform.getMcpConfig();
67846
- const claudeAccount = ctx.ops.acquireClaudeAccount();
67869
+ const claudeAccount = ctx.ops.acquireClaudeAccount(undefined, actualThreadId);
67847
67870
  if (claudeAccount) {
67848
67871
  log26.info(`Session ${sessionId.substring(0, 20)} reserved Claude account "${claudeAccount.id}"`);
67849
67872
  }
@@ -67990,11 +68013,11 @@ Please start a new session.`), { action: "Post resume failure notification" });
67990
68013
  const sessionId = ctx.ops.getSessionId(platformId, state.threadId);
67991
68014
  const resumePermissionMode = state.forceInteractivePermissions ? "default" : ctx.config.permissionMode;
67992
68015
  const platformMcpConfig = platform.getMcpConfig();
67993
- const sessionContext = buildSessionContext(platform, state.workingDir);
68016
+ const sessionContext = buildSessionContext(platform, state.workingDir, state.threadId);
67994
68017
  const appendSystemPrompt = `${sessionContext}
67995
68018
 
67996
68019
  ${CHAT_PLATFORM_PROMPT}`;
67997
- const claudeAccount = ctx.ops.acquireClaudeAccount(state.claudeAccountId);
68020
+ const claudeAccount = ctx.ops.acquireClaudeAccount(state.claudeAccountId, state.threadId);
67998
68021
  if (state.claudeAccountId && !claudeAccount) {
67999
68022
  log26.warn(`Persisted session referenced Claude account "${state.claudeAccountId}" ` + `which is no longer configured — resuming under default env`);
68000
68023
  }
@@ -68893,7 +68916,7 @@ class SessionManager extends EventEmitter4 {
68893
68916
  emitSessionAdd: (s) => this.emitSessionAdd(s),
68894
68917
  emitSessionUpdate: (sid, u) => this.emitSessionUpdate(sid, u),
68895
68918
  emitSessionRemove: (sid) => this.emitSessionRemove(sid),
68896
- acquireClaudeAccount: (preferredId) => this.accountPool.acquire(preferredId),
68919
+ acquireClaudeAccount: (preferredId, threadId) => this.accountPool.acquire(preferredId, threadId),
68897
68920
  getClaudeAccount: (id) => this.accountPool.get(id),
68898
68921
  releaseClaudeAccount: (id) => this.accountPool.release(id),
68899
68922
  markClaudeAccountCooling: (id, untilMs) => this.accountPool.markCooling(id, untilMs),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.9.4",
3
+ "version": "1.10.0",
4
4
  "description": "Share Claude Code sessions live in a Mattermost channel with interactive features",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",