claude-threads 1.16.1 → 1.16.2

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
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.16.2] - 2026-05-31
11
+
12
+ ### Fixed
13
+ - **Sturdier retry budget for the Mattermost post-save race.** Under load Mattermost can return a burst of 500s on `POST /posts` (the threads write race: duplicate key on `threads_pkey` / `app.post.save.app_error`) when several posts stream to the same thread. The retry budget was 3 attempts with plain exponential backoff; a heavy burst exhausted it and dropped a post, which surfaced as flaky task-list / sticky / context-prompt behavior. The budget is now 6 attempts with a capped (2s), equal-jittered backoff. The cap keeps the total wait bounded so a long retry chain can't itself stall things, and the jitter decorrelates concurrent posts that would otherwise re-collide on the same row lock every round. (#394)
14
+ - **Two remaining memory leaks after #351.** First, `MessageManager.dispose()` cleared the post tracker but never called `this.events.removeAllListeners()` on the per-session emitter. Each session attaches a handful of listeners (`question:complete`, `task:update`, `approval:complete`, and the rest), and their closures kept session state reachable after the session ended, so the heap grew with every session. `dispose()` now removes the listeners before resetting. Second, React 19 enables user timing when both `console.timeStamp` and `performance.measure` exist (the case on Node.js 25+), so every component re-render calls `performance.measure()` with a structured-clone'd prop-diff detail that Node buffers indefinitely (~50-205 KB per entry, ~2 GB after a long uptime). Nothing in the bot reads those entries, so a guarded `setInterval` clears them every 60 seconds with `.unref()` so it never blocks a clean exit. (#394)
15
+
10
16
  ## [1.16.1] - 2026-05-22
11
17
 
12
18
  ### Security
package/dist/index.js CHANGED
@@ -50607,8 +50607,9 @@ class MattermostClient extends BasePlatformClient {
50607
50607
  this.emit("channel_post", normalizedPost, user);
50608
50608
  }
50609
50609
  }
50610
- MAX_RETRIES = 3;
50610
+ MAX_RETRIES = 6;
50611
50611
  RETRY_DELAY_MS = 500;
50612
+ RETRY_DELAY_CAP_MS = 2000;
50612
50613
  async api(method, path, body, retryCount = 0, options) {
50613
50614
  const url = `${this.url}/api/v4${path}`;
50614
50615
  log3.debug(`API ${method} ${path}`);
@@ -50623,7 +50624,7 @@ class MattermostClient extends BasePlatformClient {
50623
50624
  if (!response.ok) {
50624
50625
  const text = await response.text();
50625
50626
  if (response.status === 500 && retryCount < this.MAX_RETRIES) {
50626
- const delay = this.RETRY_DELAY_MS * Math.pow(2, retryCount);
50627
+ const delay = this.retryDelayMs(retryCount);
50627
50628
  log3.warn(`API ${method} ${path} failed with 500, retrying in ${delay}ms (attempt ${retryCount + 1}/${this.MAX_RETRIES})`);
50628
50629
  await new Promise((resolve3) => setTimeout(resolve3, delay));
50629
50630
  return this.api(method, path, body, retryCount + 1, options);
@@ -50639,6 +50640,11 @@ class MattermostClient extends BasePlatformClient {
50639
50640
  log3.debug(`API ${method} ${path} → ${response.status}`);
50640
50641
  return response.json();
50641
50642
  }
50643
+ retryDelayMs(retryCount) {
50644
+ const ceil = Math.min(this.RETRY_DELAY_MS * Math.pow(2, retryCount), this.RETRY_DELAY_CAP_MS);
50645
+ const half = ceil / 2;
50646
+ return Math.floor(half + Math.random() * half);
50647
+ }
50642
50648
  async getBotUser() {
50643
50649
  const user = await this.api("GET", "/users/me");
50644
50650
  this.botUserId = user.id;
@@ -63796,6 +63802,7 @@ class MessageManager {
63796
63802
  dispose() {
63797
63803
  this.cancelScheduledFlush();
63798
63804
  this.postTracker.clearSession(this.sessionId);
63805
+ this.events.removeAllListeners();
63799
63806
  this.reset();
63800
63807
  }
63801
63808
  }
@@ -68733,6 +68740,17 @@ Mention me to start a session in this worktree.`, threadId);
68733
68740
  this.registry.clear();
68734
68741
  }
68735
68742
  }
68743
+ // src/utils/perf-cleanup.ts
68744
+ function startReactMeasureCleanup(intervalMs = 60000) {
68745
+ if (typeof performance === "undefined" || typeof performance.clearMeasures !== "function") {
68746
+ return null;
68747
+ }
68748
+ const timer = setInterval(() => {
68749
+ performance.clearMeasures();
68750
+ }, intervalMs);
68751
+ timer.unref?.();
68752
+ return timer;
68753
+ }
68736
68754
  // src/ui/providers/ink-provider.ts
68737
68755
  var import_react62 = __toESM(require_react(), 1);
68738
68756
 
@@ -79525,6 +79543,7 @@ async function startWithoutDaemon() {
79525
79543
  }
79526
79544
  }
79527
79545
  let triggerShutdown = null;
79546
+ startReactMeasureCleanup();
79528
79547
  const updateState = loadUpdateState();
79529
79548
  const restoredSettings = updateState.justUpdated ? updateState.runtimeSettings : undefined;
79530
79549
  if (restoredSettings) {
@@ -51553,6 +51553,7 @@ class MessageManager {
51553
51553
  dispose() {
51554
51554
  this.cancelScheduledFlush();
51555
51555
  this.postTracker.clearSession(this.sessionId);
51556
+ this.events.removeAllListeners();
51556
51557
  this.reset();
51557
51558
  }
51558
51559
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.16.1",
3
+ "version": "1.16.2",
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",