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 +8 -0
- package/dist/index.js +34 -11
- package/package.json +1 -1
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
|
-
|
|
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:
|
|
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:
|
|
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),
|