claude-threads 1.7.0 → 1.7.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 +8 -0
- package/README.md +1 -0
- package/dist/index.js +28 -9
- package/dist/mcp/permission-server.js +12 -5
- package/docs/CONFIGURATION.md +27 -0
- 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.7.1] - 2026-04-21
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **`maxSessions` cap could be exceeded under concurrent session starts** — `startSession()` passed the cap check synchronously but then awaited `createPost()` before committing the session to the map, so concurrent starts all read the same stale count and over-admitted. The integration test `"should reject new session when at capacity"` flaked at ~40% for weeks because of this. Pending starts are now tracked alongside committed sessions in the cap check. (#331)
|
|
12
|
+
- **Reject Claude accounts with both `home` and `apiKey` set** — the two are documented as mutually exclusive, but `AccountPool` silently preferred `home` when both were configured. Now the account is dropped with a warning so misconfiguration surfaces. (#330, thanks @shaders)
|
|
13
|
+
- **Tighten `reset_at` epoch regex in rate-limit detection** — the old pattern matched `"preset": N` and (more importantly) `reset_after=N`, a *relative* retry-after hint that would have been misread as an absolute epoch and pushed cooldown decades into the future. Added word-boundary anchors and regression tests. (#330)
|
|
14
|
+
- **Rate-limit emit guard now allows deadline extensions** — the boolean latch in `ClaudeCli` fired at most once per process, so a second rate-limit hit with a longer reset time couldn't widen `AccountPool.markCooling`'s extend-only window. Replaced with a numeric last-deadline tracker: repeat hits at the same severity still dedupe, but a later deadline re-emits. (#330)
|
|
15
|
+
|
|
8
16
|
## [1.7.0] - 2026-04-21
|
|
9
17
|
|
|
10
18
|
### Added
|
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
- **Git worktrees** - Isolate changes in separate branches
|
|
35
35
|
- **File attachments** - Attach images, PDFs, and files for Claude to analyze
|
|
36
36
|
- **Chrome automation** - Control Chrome browser for web tasks
|
|
37
|
+
- **Multi-account Claude (opt-in)** - Round-robin sessions across multiple Claude subscriptions or API keys with automatic rate-limit cooldown — see [Configuration](docs/CONFIGURATION.md#claude-accounts-optional-multi-account-mode)
|
|
37
38
|
|
|
38
39
|
## Quick Start
|
|
39
40
|
|
package/dist/index.js
CHANGED
|
@@ -53561,8 +53561,13 @@ class AccountPool {
|
|
|
53561
53561
|
const hasAuth = !!acc.home || !!acc.apiKey;
|
|
53562
53562
|
if (!hasAuth) {
|
|
53563
53563
|
log6.warn(`Claude account ${acc.id} has neither home nor apiKey — ignoring`);
|
|
53564
|
+
return false;
|
|
53565
|
+
}
|
|
53566
|
+
if (acc.home && acc.apiKey) {
|
|
53567
|
+
log6.warn(`Claude account ${acc.id} has both home and apiKey set — must choose one; ignoring`);
|
|
53568
|
+
return false;
|
|
53564
53569
|
}
|
|
53565
|
-
return
|
|
53570
|
+
return true;
|
|
53566
53571
|
});
|
|
53567
53572
|
this.byId = new Map(this.accounts.map((acc) => [acc.id, acc]));
|
|
53568
53573
|
for (const acc of this.accounts) {
|
|
@@ -54215,7 +54220,7 @@ function extractResetAt(text, now) {
|
|
|
54215
54220
|
};
|
|
54216
54221
|
return now + value * unitMs[unit];
|
|
54217
54222
|
}
|
|
54218
|
-
const unix = text.match(
|
|
54223
|
+
const unix = text.match(/\breset(?:_at)?\b\s*["']?\s*[:=]\s*(\d{10,13})/);
|
|
54219
54224
|
if (unix) {
|
|
54220
54225
|
const raw = parseInt(unix[1], 10);
|
|
54221
54226
|
return unix[1].length === 13 ? raw : raw * 1000;
|
|
@@ -54272,7 +54277,7 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
54272
54277
|
statusFilePath = null;
|
|
54273
54278
|
lastStatusData = null;
|
|
54274
54279
|
stderrBuffer = "";
|
|
54275
|
-
|
|
54280
|
+
lastEmittedRateLimitDeadline = 0;
|
|
54276
54281
|
log;
|
|
54277
54282
|
constructor(options2) {
|
|
54278
54283
|
super();
|
|
@@ -54324,7 +54329,7 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
54324
54329
|
if (this.process)
|
|
54325
54330
|
throw new Error("Already running");
|
|
54326
54331
|
this.stderrBuffer = "";
|
|
54327
|
-
this.
|
|
54332
|
+
this.lastEmittedRateLimitDeadline = 0;
|
|
54328
54333
|
cleanupBrowserBridgeSockets();
|
|
54329
54334
|
const claudePath = getClaudePath();
|
|
54330
54335
|
const args = [
|
|
@@ -54479,9 +54484,11 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
54479
54484
|
const hit = detectRateLimit(text);
|
|
54480
54485
|
if (!hit.detected)
|
|
54481
54486
|
return;
|
|
54482
|
-
|
|
54487
|
+
const newDeadline = cooldownDeadline(hit);
|
|
54488
|
+
const MIN_ADVANCE_MS = 60000;
|
|
54489
|
+
if (newDeadline - this.lastEmittedRateLimitDeadline < MIN_ADVANCE_MS)
|
|
54483
54490
|
return;
|
|
54484
|
-
this.
|
|
54491
|
+
this.lastEmittedRateLimitDeadline = newDeadline;
|
|
54485
54492
|
this.log.warn(`Rate limit detected: ${hit.matched ?? "(no match text)"}`);
|
|
54486
54493
|
this.emit("rate-limit", hit);
|
|
54487
54494
|
}
|
|
@@ -67216,6 +67223,11 @@ var sessionLog6 = createSessionLog(log26);
|
|
|
67216
67223
|
function mutableSessions(ctx) {
|
|
67217
67224
|
return ctx.state.sessions;
|
|
67218
67225
|
}
|
|
67226
|
+
var pendingStartsCount = 0;
|
|
67227
|
+
function releasePendingStart() {
|
|
67228
|
+
if (pendingStartsCount > 0)
|
|
67229
|
+
pendingStartsCount--;
|
|
67230
|
+
}
|
|
67219
67231
|
function mutablePostIndex(ctx) {
|
|
67220
67232
|
return ctx.state.postIndex;
|
|
67221
67233
|
}
|
|
@@ -67548,20 +67560,24 @@ async function startSession(options2, username, displayName, replyToPostId, plat
|
|
|
67548
67560
|
if (!platform) {
|
|
67549
67561
|
throw new Error(`Platform '${platformId}' not found. Call addPlatform() first.`);
|
|
67550
67562
|
}
|
|
67551
|
-
|
|
67563
|
+
const activeOrPending = ctx.state.sessions.size + pendingStartsCount;
|
|
67564
|
+
if (activeOrPending >= ctx.config.maxSessions) {
|
|
67552
67565
|
const formatter2 = platform.getFormatter();
|
|
67553
67566
|
const tempSession = {
|
|
67554
67567
|
platform,
|
|
67555
67568
|
threadId: replyToPostId || "",
|
|
67556
67569
|
sessionId: "temp"
|
|
67557
67570
|
};
|
|
67558
|
-
await post(tempSession, "warning", `${formatter2.formatBold("Too busy")} - ${
|
|
67571
|
+
await post(tempSession, "warning", `${formatter2.formatBold("Too busy")} - ${activeOrPending} sessions active. Please try again later.`);
|
|
67559
67572
|
return;
|
|
67560
67573
|
}
|
|
67574
|
+
pendingStartsCount++;
|
|
67561
67575
|
const startFormatter = platform.getFormatter();
|
|
67562
67576
|
const startPost = await withErrorHandling(() => platform.createPost(startFormatter.formatItalic("Claude Threads session starting..."), replyToPostId), { action: "Create session post" });
|
|
67563
|
-
if (!startPost)
|
|
67577
|
+
if (!startPost) {
|
|
67578
|
+
releasePendingStart();
|
|
67564
67579
|
return;
|
|
67580
|
+
}
|
|
67565
67581
|
const actualThreadId = replyToPostId || startPost.id;
|
|
67566
67582
|
const sessionId = ctx.ops.getSessionId(platformId, actualThreadId);
|
|
67567
67583
|
platform.sendTyping(actualThreadId);
|
|
@@ -67576,11 +67592,13 @@ async function startSession(options2, username, displayName, replyToPostId, plat
|
|
|
67576
67592
|
const resolvedDir = resolve6(requestedDir);
|
|
67577
67593
|
if (!existsSync11(resolvedDir)) {
|
|
67578
67594
|
await platform.updatePost(startPost.id, `❌ Directory does not exist: ${formatter.formatCode(initialOptions.workingDir)}`);
|
|
67595
|
+
releasePendingStart();
|
|
67579
67596
|
return;
|
|
67580
67597
|
}
|
|
67581
67598
|
const { statSync: statSync4 } = await import("fs");
|
|
67582
67599
|
if (!statSync4(resolvedDir).isDirectory()) {
|
|
67583
67600
|
await platform.updatePost(startPost.id, `❌ Not a directory: ${formatter.formatCode(initialOptions.workingDir)}`);
|
|
67601
|
+
releasePendingStart();
|
|
67584
67602
|
return;
|
|
67585
67603
|
}
|
|
67586
67604
|
workingDir = resolvedDir;
|
|
@@ -67649,6 +67667,7 @@ ${CHAT_PLATFORM_PROMPT}`;
|
|
|
67649
67667
|
workingDir: ctx.config.workingDir
|
|
67650
67668
|
});
|
|
67651
67669
|
mutableSessions(ctx).set(sessionId, session);
|
|
67670
|
+
releasePendingStart();
|
|
67652
67671
|
ctx.ops.registerPost(startPost.id, actualThreadId);
|
|
67653
67672
|
ctx.ops.emitSessionAdd(session);
|
|
67654
67673
|
sessionLog6(session).info(`▶ Session started by @${username}`);
|
|
@@ -53526,6 +53526,11 @@ function detectRateLimit(text, now = Date.now()) {
|
|
|
53526
53526
|
const resetAtEpochMs = extractResetAt(text, now);
|
|
53527
53527
|
return { detected: true, matched, resetAtEpochMs };
|
|
53528
53528
|
}
|
|
53529
|
+
function cooldownDeadline(hit, now = Date.now()) {
|
|
53530
|
+
if (!hit.detected)
|
|
53531
|
+
return now;
|
|
53532
|
+
return hit.resetAtEpochMs ?? now + DEFAULT_COOLDOWN_MS;
|
|
53533
|
+
}
|
|
53529
53534
|
function extractResetAt(text, now) {
|
|
53530
53535
|
const relative = text.match(/(?:retry[_\s-]?after|resets?\s+in)\s+(\d+)\s*(second|minute|hour|day)s?/i);
|
|
53531
53536
|
if (relative) {
|
|
@@ -53539,7 +53544,7 @@ function extractResetAt(text, now) {
|
|
|
53539
53544
|
};
|
|
53540
53545
|
return now + value * unitMs[unit];
|
|
53541
53546
|
}
|
|
53542
|
-
const unix = text.match(
|
|
53547
|
+
const unix = text.match(/\breset(?:_at)?\b\s*["']?\s*[:=]\s*(\d{10,13})/);
|
|
53543
53548
|
if (unix) {
|
|
53544
53549
|
const raw = parseInt(unix[1], 10);
|
|
53545
53550
|
return unix[1].length === 13 ? raw : raw * 1000;
|
|
@@ -53596,7 +53601,7 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
53596
53601
|
statusFilePath = null;
|
|
53597
53602
|
lastStatusData = null;
|
|
53598
53603
|
stderrBuffer = "";
|
|
53599
|
-
|
|
53604
|
+
lastEmittedRateLimitDeadline = 0;
|
|
53600
53605
|
log;
|
|
53601
53606
|
constructor(options2) {
|
|
53602
53607
|
super();
|
|
@@ -53648,7 +53653,7 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
53648
53653
|
if (this.process)
|
|
53649
53654
|
throw new Error("Already running");
|
|
53650
53655
|
this.stderrBuffer = "";
|
|
53651
|
-
this.
|
|
53656
|
+
this.lastEmittedRateLimitDeadline = 0;
|
|
53652
53657
|
cleanupBrowserBridgeSockets();
|
|
53653
53658
|
const claudePath = getClaudePath();
|
|
53654
53659
|
const args = [
|
|
@@ -53803,9 +53808,11 @@ class ClaudeCli extends EventEmitter2 {
|
|
|
53803
53808
|
const hit = detectRateLimit(text);
|
|
53804
53809
|
if (!hit.detected)
|
|
53805
53810
|
return;
|
|
53806
|
-
|
|
53811
|
+
const newDeadline = cooldownDeadline(hit);
|
|
53812
|
+
const MIN_ADVANCE_MS = 60000;
|
|
53813
|
+
if (newDeadline - this.lastEmittedRateLimitDeadline < MIN_ADVANCE_MS)
|
|
53807
53814
|
return;
|
|
53808
|
-
this.
|
|
53815
|
+
this.lastEmittedRateLimitDeadline = newDeadline;
|
|
53809
53816
|
this.log.warn(`Rate limit detected: ${hit.matched ?? "(no match text)"}`);
|
|
53810
53817
|
this.emit("rate-limit", hit);
|
|
53811
53818
|
}
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -72,6 +72,33 @@ platforms:
|
|
|
72
72
|
| `allowedUsers` | No | List of Slack usernames |
|
|
73
73
|
| `skipPermissions` | No | Auto-approve actions (default: `false`) |
|
|
74
74
|
|
|
75
|
+
## Claude Accounts (optional, multi-account mode)
|
|
76
|
+
|
|
77
|
+
By default every session spawns `claude` with the bot's own `process.env`, so they all share one subscription's token budget. Add a `claudeAccounts` block to spread load across multiple accounts — the bot round-robins new sessions across the pool and automatically skips accounts in rate-limit cooldown. Omit the block entirely to stay in single-account mode (unchanged behavior).
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
claudeAccounts:
|
|
81
|
+
# OAuth accounts — prepare each HOME first with `HOME=<path> claude login`
|
|
82
|
+
- id: primary
|
|
83
|
+
home: /home/bot/.claude-accounts/primary
|
|
84
|
+
- id: backup
|
|
85
|
+
displayName: Backup (Pro)
|
|
86
|
+
home: /home/bot/.claude-accounts/backup
|
|
87
|
+
|
|
88
|
+
# API-key billed
|
|
89
|
+
- id: shared-api
|
|
90
|
+
apiKey: sk-ant-api03-xxxxxxxx...
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
| Setting | Required | Description |
|
|
94
|
+
|---------|----------|-------------|
|
|
95
|
+
| `id` | Yes | Stable identifier used in logs, UI, and persisted session state |
|
|
96
|
+
| `home` | One of | Alternate `$HOME` containing `.claude/.credentials.json` from a prior `HOME=<path> claude login`. For OAuth Pro/Max subscriptions. Session history also lives here, so resumed sessions pick the same account. |
|
|
97
|
+
| `apiKey` | One of | Anthropic API key. Billed against that key; session history stays under the bot's default `HOME`. |
|
|
98
|
+
| `displayName` | No | Human-readable label in UI (defaults to `id`) |
|
|
99
|
+
|
|
100
|
+
Exactly one of `home` or `apiKey` should be set per account. Persisted sessions record which account they ran under and resume on the same one.
|
|
101
|
+
|
|
75
102
|
## Environment Variables
|
|
76
103
|
|
|
77
104
|
| Variable | Description | Default |
|