claude-threads 1.9.3 → 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 +13 -0
- package/dist/index.js +39 -11
- package/dist/mcp/permission-server.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ 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
|
+
|
|
16
|
+
## [1.9.4] - 2026-04-29
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **Memory leak across session lifecycles.** `PostTracker` registered every post created during a session but its `clearSession(sessionId)` was only invoked on full bot shutdown. Across many sessions the in-memory map grew without bound and contributed to V8 mark-compact becoming ineffective and an eventual OOM abort (observed at ~14 h uptime). `MessageManager.dispose()` now clears its session's `PostTracker` bucket, and dispose is wired into all five session-removal paths in `src/session/lifecycle.ts`: normal exit, kill, idle timeout, pause, shutdown, early exit, resume-fail, plus the start-failure and resume-failure error branches that bypass the shared cleanup helpers. (#356, fixes #351)
|
|
20
|
+
|
|
8
21
|
## [1.9.3] - 2026-04-24
|
|
9
22
|
|
|
10
23
|
### Internals
|
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));
|
|
@@ -60991,6 +61008,7 @@ class MessageManager {
|
|
|
60991
61008
|
}
|
|
60992
61009
|
dispose() {
|
|
60993
61010
|
this.cancelScheduledFlush();
|
|
61011
|
+
this.postTracker.clearSession(this.sessionId);
|
|
60994
61012
|
this.reset();
|
|
60995
61013
|
}
|
|
60996
61014
|
}
|
|
@@ -65911,7 +65929,7 @@ async function changeDirectory(session, newDir, username, ctx) {
|
|
|
65911
65929
|
session.workingDir = absoluteDir;
|
|
65912
65930
|
const newSessionId = randomUUID2();
|
|
65913
65931
|
session.claudeSessionId = newSessionId;
|
|
65914
|
-
const sessionContext = buildSessionContext(session.platform, absoluteDir);
|
|
65932
|
+
const sessionContext = buildSessionContext(session.platform, absoluteDir, session.threadId);
|
|
65915
65933
|
const appendSystemPrompt = `${sessionContext}
|
|
65916
65934
|
|
|
65917
65935
|
${CHAT_PLATFORM_PROMPT}`;
|
|
@@ -66614,6 +66632,7 @@ async function createAndSwitchToWorktree(session, branch, username, options2) {
|
|
|
66614
66632
|
const newSessionId = randomUUID3();
|
|
66615
66633
|
session.claudeSessionId = newSessionId;
|
|
66616
66634
|
const needsTitlePrompt = !session.sessionTitle;
|
|
66635
|
+
const sessionContext = needsTitlePrompt ? buildSessionContext(session.platform, existing.path, session.threadId) : null;
|
|
66617
66636
|
const cliOptions = {
|
|
66618
66637
|
workingDir: existing.path,
|
|
66619
66638
|
threadId: session.threadId,
|
|
@@ -66626,7 +66645,9 @@ async function createAndSwitchToWorktree(session, branch, username, options2) {
|
|
|
66626
66645
|
resume: false,
|
|
66627
66646
|
chrome: options2.chromeEnabled,
|
|
66628
66647
|
platformConfig: session.platform.getMcpConfig(),
|
|
66629
|
-
appendSystemPrompt:
|
|
66648
|
+
appendSystemPrompt: sessionContext ? `${sessionContext}
|
|
66649
|
+
|
|
66650
|
+
${options2.appendSystemPrompt}` : undefined,
|
|
66630
66651
|
logSessionId: session.sessionId,
|
|
66631
66652
|
permissionTimeoutMs: options2.permissionTimeoutMs
|
|
66632
66653
|
};
|
|
@@ -66707,6 +66728,7 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
|
|
|
66707
66728
|
const newSessionId = randomUUID3();
|
|
66708
66729
|
session.claudeSessionId = newSessionId;
|
|
66709
66730
|
const needsTitlePrompt = !session.sessionTitle;
|
|
66731
|
+
const sessionContext = needsTitlePrompt ? buildSessionContext(session.platform, worktreePath, session.threadId) : null;
|
|
66710
66732
|
const cliOptions = {
|
|
66711
66733
|
workingDir: worktreePath,
|
|
66712
66734
|
threadId: session.threadId,
|
|
@@ -66719,7 +66741,9 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
|
|
|
66719
66741
|
resume: false,
|
|
66720
66742
|
chrome: options2.chromeEnabled,
|
|
66721
66743
|
platformConfig: session.platform.getMcpConfig(),
|
|
66722
|
-
appendSystemPrompt:
|
|
66744
|
+
appendSystemPrompt: sessionContext ? `${sessionContext}
|
|
66745
|
+
|
|
66746
|
+
${options2.appendSystemPrompt}` : undefined,
|
|
66723
66747
|
logSessionId: session.sessionId,
|
|
66724
66748
|
permissionTimeoutMs: options2.permissionTimeoutMs
|
|
66725
66749
|
};
|
|
@@ -67485,6 +67509,7 @@ async function cleanupSession(session, ctx, options2 = {}) {
|
|
|
67485
67509
|
if (doCloseLogger) {
|
|
67486
67510
|
await closeThreadLogger(session, action, details);
|
|
67487
67511
|
}
|
|
67512
|
+
session.messageManager?.dispose();
|
|
67488
67513
|
ctx.ops.emitSessionRemove(session.sessionId);
|
|
67489
67514
|
mutableSessions(ctx).delete(session.sessionId);
|
|
67490
67515
|
if (doCleanupPostIndex) {
|
|
@@ -67500,6 +67525,7 @@ function releaseAccountIfHeld(session, ctx) {
|
|
|
67500
67525
|
}
|
|
67501
67526
|
}
|
|
67502
67527
|
function removeFromRegistry(session, ctx) {
|
|
67528
|
+
session.messageManager?.dispose();
|
|
67503
67529
|
ctx.ops.emitSessionRemove(session.sessionId);
|
|
67504
67530
|
mutableSessions(ctx).delete(session.sessionId);
|
|
67505
67531
|
cleanupPostIndex(ctx, session.threadId);
|
|
@@ -67835,12 +67861,12 @@ async function startSession(options2, username, displayName, replyToPostId, plat
|
|
|
67835
67861
|
permissionMode = "default";
|
|
67836
67862
|
log26.info(`Starting session with interactive permissions (from !permissions command)`);
|
|
67837
67863
|
}
|
|
67838
|
-
const sessionContext = buildSessionContext(platform, workingDir);
|
|
67864
|
+
const sessionContext = buildSessionContext(platform, workingDir, actualThreadId);
|
|
67839
67865
|
const systemPrompt = `${sessionContext}
|
|
67840
67866
|
|
|
67841
67867
|
${CHAT_PLATFORM_PROMPT}`;
|
|
67842
67868
|
const platformMcpConfig = platform.getMcpConfig();
|
|
67843
|
-
const claudeAccount = ctx.ops.acquireClaudeAccount();
|
|
67869
|
+
const claudeAccount = ctx.ops.acquireClaudeAccount(undefined, actualThreadId);
|
|
67844
67870
|
if (claudeAccount) {
|
|
67845
67871
|
log26.info(`Session ${sessionId.substring(0, 20)} reserved Claude account "${claudeAccount.id}"`);
|
|
67846
67872
|
}
|
|
@@ -67911,6 +67937,7 @@ ${CHAT_PLATFORM_PROMPT}`;
|
|
|
67911
67937
|
} catch (err) {
|
|
67912
67938
|
await logAndNotify(err, { action: "Start Claude", session });
|
|
67913
67939
|
ctx.ops.stopTyping(session);
|
|
67940
|
+
session.messageManager?.dispose();
|
|
67914
67941
|
ctx.ops.emitSessionRemove(session.sessionId);
|
|
67915
67942
|
mutableSessions(ctx).delete(session.sessionId);
|
|
67916
67943
|
releaseAccountIfHeld(session, ctx);
|
|
@@ -67986,11 +68013,11 @@ Please start a new session.`), { action: "Post resume failure notification" });
|
|
|
67986
68013
|
const sessionId = ctx.ops.getSessionId(platformId, state.threadId);
|
|
67987
68014
|
const resumePermissionMode = state.forceInteractivePermissions ? "default" : ctx.config.permissionMode;
|
|
67988
68015
|
const platformMcpConfig = platform.getMcpConfig();
|
|
67989
|
-
const sessionContext = buildSessionContext(platform, state.workingDir);
|
|
68016
|
+
const sessionContext = buildSessionContext(platform, state.workingDir, state.threadId);
|
|
67990
68017
|
const appendSystemPrompt = `${sessionContext}
|
|
67991
68018
|
|
|
67992
68019
|
${CHAT_PLATFORM_PROMPT}`;
|
|
67993
|
-
const claudeAccount = ctx.ops.acquireClaudeAccount(state.claudeAccountId);
|
|
68020
|
+
const claudeAccount = ctx.ops.acquireClaudeAccount(state.claudeAccountId, state.threadId);
|
|
67994
68021
|
if (state.claudeAccountId && !claudeAccount) {
|
|
67995
68022
|
log26.warn(`Persisted session referenced Claude account "${state.claudeAccountId}" ` + `which is no longer configured — resuming under default env`);
|
|
67996
68023
|
}
|
|
@@ -68114,6 +68141,7 @@ ${sessionFormatter.formatItalic("Reconnected to Claude session. You can continue
|
|
|
68114
68141
|
ctx.ops.persistSession(session);
|
|
68115
68142
|
} catch (err) {
|
|
68116
68143
|
log26.error(`Failed to resume session ${shortId}`, err instanceof Error ? err : undefined);
|
|
68144
|
+
session.messageManager?.dispose();
|
|
68117
68145
|
ctx.ops.emitSessionRemove(sessionId);
|
|
68118
68146
|
mutableSessions(ctx).delete(sessionId);
|
|
68119
68147
|
ctx.state.sessionStore.remove(sessionId);
|
|
@@ -68888,7 +68916,7 @@ class SessionManager extends EventEmitter4 {
|
|
|
68888
68916
|
emitSessionAdd: (s) => this.emitSessionAdd(s),
|
|
68889
68917
|
emitSessionUpdate: (sid, u) => this.emitSessionUpdate(sid, u),
|
|
68890
68918
|
emitSessionRemove: (sid) => this.emitSessionRemove(sid),
|
|
68891
|
-
acquireClaudeAccount: (preferredId) => this.accountPool.acquire(preferredId),
|
|
68919
|
+
acquireClaudeAccount: (preferredId, threadId) => this.accountPool.acquire(preferredId, threadId),
|
|
68892
68920
|
getClaudeAccount: (id) => this.accountPool.get(id),
|
|
68893
68921
|
releaseClaudeAccount: (id) => this.accountPool.release(id),
|
|
68894
68922
|
markClaudeAccountCooling: (id, untilMs) => this.accountPool.markCooling(id, untilMs),
|
|
@@ -52899,6 +52899,7 @@ class MessageManager {
|
|
|
52899
52899
|
}
|
|
52900
52900
|
dispose() {
|
|
52901
52901
|
this.cancelScheduledFlush();
|
|
52902
|
+
this.postTracker.clearSession(this.sessionId);
|
|
52902
52903
|
this.reset();
|
|
52903
52904
|
}
|
|
52904
52905
|
}
|