claude-threads 1.39.2 → 1.39.3

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,12 @@ 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.39.3] - 2026-10-01
9
+
10
+ ### Fixed
11
+ - **Mattermost: a skin-toned 👍 no longer denies** (#629). Mattermost names an emoji picked with a skin tone `+1_medium_skin_tone` (verified against Mattermost v11), and the permission prompt treats any emoji it does not recognize as a denial. 1.39.2 fixed Slack's `+1::skin-tone-3` form; the Mattermost form is now matched on its base name too, for permission prompts, plan approvals and the other reaction controls.
12
+ - **One failed update no longer turns a code block into a post per flush** (#620). When the platform refused an update of the post Claude was writing a code block into, the bot gave up on that post at once. The continuation started in the middle of the block without its opening fence, so the rest rendered as prose, the height check split it every few lines (a new post per flush, close to 200 posts for a 2000-line file), and the closing fence opened a new block. A failed update is now retried on the next flush, which recovers the post completely for a passing error; only after three failures in a row is the post given up, with its code block reopened at the start of the next one.
13
+
8
14
  ## [1.39.2] - 2026-10-01
9
15
 
10
16
  ### Fixed
package/dist/index.js CHANGED
@@ -6033,7 +6033,7 @@ var init_usage = __esm(() => {
6033
6033
 
6034
6034
  // src/utils/emoji.ts
6035
6035
  function withoutSkinTone(emoji) {
6036
- return emoji.replace(/::skin-tone-\d$/, "");
6036
+ return emoji.replace(/::skin-tone-\d$/, "").replace(/(?<=.)_(?:light|medium_light|medium|medium_dark|dark)_skin_tone$/, "");
6037
6037
  }
6038
6038
  function isApprovalEmoji(emoji) {
6039
6039
  return APPROVAL_EMOJIS.includes(withoutSkinTone(emoji));
@@ -60297,7 +60297,8 @@ class ContentExecutor extends BaseExecutor {
60297
60297
  headerPostId: null,
60298
60298
  headerBody: "",
60299
60299
  turnOpen: false,
60300
- paragraphBreak: false
60300
+ paragraphBreak: false,
60301
+ updateFailures: 0
60301
60302
  };
60302
60303
  }
60303
60304
  setHeader(line) {
@@ -60318,6 +60319,7 @@ class ContentExecutor extends BaseExecutor {
60318
60319
  this.state.headerPostId = null;
60319
60320
  this.state.headerBody = "";
60320
60321
  this.state.turnOpen = false;
60322
+ this.state.updateFailures = 0;
60321
60323
  }
60322
60324
  getRenderedCurrentPost() {
60323
60325
  const postId = this.state.currentPostId;
@@ -60344,7 +60346,9 @@ ${body}` : this.state.header;
60344
60346
  this.state.headerDirty = false;
60345
60347
  if (this.state.headerPostId) {
60346
60348
  const postId = this.state.headerPostId;
60349
+ const failures = this.state.updateFailures;
60347
60350
  await this.tryUpdatePost(ctx, postId, this.state.headerBody, "header", { reason: "header_update", headerLength: this.state.header?.length ?? 0 }, { reason: "header_update_failed" }, () => {}, () => {});
60351
+ this.state.updateFailures = failures;
60348
60352
  return;
60349
60353
  }
60350
60354
  if (this.state.header && this.state.turnOpen) {
@@ -60367,6 +60371,7 @@ ${body}` : this.state.header;
60367
60371
  this.state.headerBody = content;
60368
60372
  this.state.headerDirty = false;
60369
60373
  }
60374
+ this.state.updateFailures = 0;
60370
60375
  onSuccess();
60371
60376
  ctx.threadLogger?.logExecutor("content", "update", postId, successDetails, logTag);
60372
60377
  } catch (err) {
@@ -60379,8 +60384,11 @@ ${body}` : this.state.header;
60379
60384
  closeCurrentPost(ctx) {
60380
60385
  const oldPostId = this.state.currentPostId;
60381
60386
  const contentLength = this.state.currentPostContent.length;
60387
+ if (this.state.updateFailures > 0 && this.state.pendingContent.trim())
60388
+ this.giveUpCurrentPost();
60382
60389
  this.state.currentPostId = null;
60383
60390
  this.state.currentPostContent = "";
60391
+ this.state.updateFailures = 0;
60384
60392
  if (ctx?.threadLogger && oldPostId) {
60385
60393
  ctx.threadLogger.logExecutor("content", "close", oldPostId, {
60386
60394
  contentLength,
@@ -60436,13 +60444,22 @@ ${body}` : this.state.header;
60436
60444
  this.state.turnOpen = false;
60437
60445
  return;
60438
60446
  }
60439
- await this.flushPending(ctx);
60440
- if (this.state.headerDirty)
60447
+ const postBefore = this.state.currentPostId;
60448
+ await this.flushPending(ctx, reason);
60449
+ if (reason === "result") {
60450
+ if (this.state.updateFailures > 0 && this.state.currentPostId !== null && this.state.pendingContent.trim()) {
60451
+ await this.flushPending(ctx, reason);
60452
+ }
60453
+ if (postBefore !== null && this.state.currentPostId === null && this.state.pendingContent.trim()) {
60454
+ await this.flushPending(ctx, reason);
60455
+ }
60456
+ }
60457
+ if (this.state.headerDirty && this.state.updateFailures === 0)
60441
60458
  await this.renderHeaderOnly(ctx);
60442
60459
  if (reason === "result")
60443
60460
  this.state.turnOpen = false;
60444
60461
  }
60445
- async flushPending(ctx) {
60462
+ async flushPending(ctx, reason) {
60446
60463
  const pendingAtFlushStart = this.state.pendingContent;
60447
60464
  let content = ctx.formatter.formatMarkdown(pendingAtFlushStart).trim();
60448
60465
  const { maxLength: MAX_POST_LENGTH, hardThreshold: HARD_CONTINUATION_THRESHOLD } = ctx.platform.getMessageLimits();
@@ -60460,7 +60477,7 @@ ${body}` : this.state.header;
60460
60477
  const shouldBreakEarly = this.state.currentPostId && combinedContent.length > MIN_BREAK_THRESHOLD && ctx.contentBreaker.shouldFlushEarly(combinedContent);
60461
60478
  const reserve = this.headerReserve(this.state.currentPostId);
60462
60479
  if (this.state.currentPostId && (combinedContent.length + reserve > HARD_CONTINUATION_THRESHOLD || shouldBreakEarly)) {
60463
- await this.handleSplit(ctx, combinedContent, pendingAtFlushStart, HARD_CONTINUATION_THRESHOLD);
60480
+ await this.handleSplit(ctx, combinedContent, pendingAtFlushStart, HARD_CONTINUATION_THRESHOLD, reason);
60464
60481
  return;
60465
60482
  }
60466
60483
  if (this.state.currentPostId && content.length + reserve > MAX_POST_LENGTH) {
@@ -60495,10 +60512,7 @@ ${body}` : this.state.header;
60495
60512
  await this.tryUpdatePost(ctx, postId, combinedContent, "flush", { newContentLength: content.length, combinedLength: combinedContent.length }, (err) => ({ failedOp: "updatePost", error: String(err) }), () => {
60496
60513
  this.state.currentPostContent = combinedContent;
60497
60514
  this.clearFlushedContent(pendingAtFlushStart);
60498
- }, () => {
60499
- this.state.currentPostId = null;
60500
- this.state.currentPostContent = "";
60501
- });
60515
+ }, () => this.onInPlaceUpdateFailed(reason));
60502
60516
  } else {
60503
60517
  const chunks = splitContentForHeight(content, ctx.contentBreaker).flatMap((chunk) => chunk.length + reserve > MAX_POST_LENGTH ? splitByLength(chunk, HARD_CONTINUATION_THRESHOLD - reserve) : [chunk]);
60504
60518
  ctx.threadLogger?.logExecutor("content", "create_start", "none", {
@@ -60509,7 +60523,7 @@ ${body}` : this.state.header;
60509
60523
  await this.postChunks(ctx, chunks, pendingAtFlushStart, false);
60510
60524
  }
60511
60525
  }
60512
- async splitOversized(ctx, content, pendingAtFlushStart, hardThreshold) {
60526
+ async splitOversized(ctx, content, pendingAtFlushStart, hardThreshold, reason) {
60513
60527
  const postId = this.state.currentPostId;
60514
60528
  if (!postId)
60515
60529
  return false;
@@ -60520,17 +60534,35 @@ ${body}` : this.state.header;
60520
60534
  let landed = false;
60521
60535
  await this.tryUpdatePost(ctx, postId, firstPiece, "handleSplit", { reason: "oversized_code_block", contentLength: content.length, pieces: rest.length + 1 }, { reason: "oversized_code_block_failed" }, () => {
60522
60536
  landed = true;
60523
- }, () => {
60524
- if (postId === this.state.headerPostId)
60525
- this.state.headerBody = firstPiece;
60526
- });
60537
+ }, () => {});
60538
+ if (!landed) {
60539
+ this.onInPlaceUpdateFailed(reason);
60540
+ return true;
60541
+ }
60527
60542
  this.state.currentPostId = null;
60528
60543
  this.state.currentPostContent = "";
60529
- if (landed)
60530
- this.clearFlushedContent(pendingAtFlushStart);
60531
- await this.postChunks(ctx, rest, pendingAtFlushStart, landed);
60544
+ this.clearFlushedContent(pendingAtFlushStart);
60545
+ await this.postChunks(ctx, rest, pendingAtFlushStart, true);
60532
60546
  return true;
60533
60547
  }
60548
+ onInPlaceUpdateFailed(reason) {
60549
+ this.state.updateFailures++;
60550
+ const limit = reason === "result" ? 2 : MAX_IN_PLACE_UPDATE_FAILURES;
60551
+ if (this.state.updateFailures < limit)
60552
+ return;
60553
+ this.giveUpCurrentPost();
60554
+ }
60555
+ giveUpCurrentPost() {
60556
+ const opener = openFenceOf(this.state.currentPostContent);
60557
+ this.state.currentPostId = null;
60558
+ this.state.currentPostContent = "";
60559
+ this.state.updateFailures = 0;
60560
+ if (!opener)
60561
+ return;
60562
+ const rest = this.state.pendingContent.replace(/^\n+/, "");
60563
+ this.state.pendingContent = /^```[ \t]*(\n|$)/.test(rest) ? rest.replace(/^```[ \t]*\n?/, "") : `${opener}
60564
+ ${rest}`;
60565
+ }
60534
60566
  async postChunks(ctx, chunks, pendingAtFlushStart, flushCleared) {
60535
60567
  let cleared = flushCleared;
60536
60568
  for (let i = 0;i < chunks.length; i++) {
@@ -60554,7 +60586,7 @@ ${this.state.pendingContent}` : "");
60554
60586
  }
60555
60587
  }
60556
60588
  }
60557
- async handleSplit(ctx, content, pendingAtFlushStart, hardThreshold) {
60589
+ async handleSplit(ctx, content, pendingAtFlushStart, hardThreshold, reason) {
60558
60590
  let breakPoint;
60559
60591
  let codeBlockOpenPosition;
60560
60592
  if (content.length > hardThreshold) {
@@ -60602,26 +60634,21 @@ ${this.state.pendingContent}` : "");
60602
60634
  await this.tryUpdatePost(ctx, postId, content, "handleSplit", { reason: "soft_break_no_breakpoint", contentLength: content.length }, { reason: "soft_break_no_breakpoint_failed" }, () => {
60603
60635
  this.state.currentPostContent = content;
60604
60636
  this.clearFlushedContent(pendingAtFlushStart);
60605
- }, () => {
60606
- this.state.currentPostId = null;
60607
- });
60637
+ }, () => this.onInPlaceUpdateFailed(reason));
60608
60638
  }
60609
60639
  return;
60610
60640
  }
60611
60641
  }
60612
60642
  if (codeBlockOpenPosition !== undefined) {
60613
60643
  if (codeBlockOpenPosition === 0) {
60614
- if (await this.splitOversized(ctx, content, pendingAtFlushStart, hardThreshold))
60644
+ if (await this.splitOversized(ctx, content, pendingAtFlushStart, hardThreshold, reason))
60615
60645
  return;
60616
60646
  if (this.state.currentPostId) {
60617
60647
  const postId = this.state.currentPostId;
60618
60648
  await this.tryUpdatePost(ctx, postId, content, "handleSplit", { reason: "code_block_at_start", contentLength: content.length }, { reason: "code_block_at_start_failed" }, () => {
60619
60649
  this.state.currentPostContent = content;
60620
60650
  this.clearFlushedContent(pendingAtFlushStart);
60621
- }, () => {
60622
- this.state.currentPostId = null;
60623
- this.state.currentPostContent = "";
60624
- });
60651
+ }, () => this.onInPlaceUpdateFailed(reason));
60625
60652
  }
60626
60653
  return;
60627
60654
  }
@@ -60630,17 +60657,14 @@ ${this.state.pendingContent}` : "");
60630
60657
  if (breakBeforeCodeBlock > 0) {
60631
60658
  breakPoint = breakBeforeCodeBlock;
60632
60659
  } else {
60633
- if (await this.splitOversized(ctx, content, pendingAtFlushStart, hardThreshold))
60660
+ if (await this.splitOversized(ctx, content, pendingAtFlushStart, hardThreshold, reason))
60634
60661
  return;
60635
60662
  if (this.state.currentPostId) {
60636
60663
  const postId = this.state.currentPostId;
60637
60664
  await this.tryUpdatePost(ctx, postId, content, "handleSplit", { reason: "no_break_before_code_block", contentLength: content.length }, { reason: "no_break_before_code_block_failed" }, () => {
60638
60665
  this.state.currentPostContent = content;
60639
60666
  this.clearFlushedContent(pendingAtFlushStart);
60640
- }, () => {
60641
- this.state.currentPostId = null;
60642
- this.state.currentPostContent = "";
60643
- });
60667
+ }, () => this.onInPlaceUpdateFailed(reason));
60644
60668
  }
60645
60669
  return;
60646
60670
  }
@@ -60652,11 +60676,11 @@ ${this.state.pendingContent}` : "");
60652
60676
  const postId = this.state.currentPostId;
60653
60677
  await this.tryUpdatePost(ctx, postId, firstPart, "handleSplit", { reason: "split_first_part", firstPartLength: firstPart.length, remainderLength: remainder.length }, { reason: "split_first_part_failed" }, () => {
60654
60678
  firstPartLanded = true;
60655
- }, () => {
60656
- if (postId === this.state.headerPostId) {
60657
- this.state.headerBody = firstPart;
60658
- }
60659
- });
60679
+ }, () => {});
60680
+ if (!firstPartLanded) {
60681
+ this.onInPlaceUpdateFailed(reason);
60682
+ return;
60683
+ }
60660
60684
  }
60661
60685
  this.state.currentPostId = null;
60662
60686
  this.state.currentPostContent = "";
@@ -60689,6 +60713,7 @@ ${content}` : header : content;
60689
60713
  if (bumpedPostId) {
60690
60714
  adoptAsHeaderPost(bumpedPostId);
60691
60715
  this.state.currentPostId = bumpedPostId;
60716
+ this.state.updateFailures = 0;
60692
60717
  this.state.currentPostContent = content;
60693
60718
  this.clearFlushedContent(pendingAtFlushStart);
60694
60719
  ctx.threadLogger?.logExecutor("content", "create", bumpedPostId, {
@@ -60705,6 +60730,7 @@ ${content}` : header : content;
60705
60730
  const post = await ctx.createPost(rendered, { type: "content" });
60706
60731
  adoptAsHeaderPost(post.id);
60707
60732
  this.state.currentPostId = post.id;
60733
+ this.state.updateFailures = 0;
60708
60734
  this.state.currentPostContent = content;
60709
60735
  this.clearFlushedContent(pendingAtFlushStart);
60710
60736
  ctx.logger.debug(`Created post ${formatShortId(post.id)}`);
@@ -60730,6 +60756,11 @@ ${content}` : header : content;
60730
60756
  }
60731
60757
  }
60732
60758
  var FENCE_LINE = /^```.*$/gm;
60759
+ var MAX_IN_PLACE_UPDATE_FAILURES = 3;
60760
+ function openFenceOf(text) {
60761
+ const fences = text.match(FENCE_LINE) ?? [];
60762
+ return fences.length % 2 === 1 ? fences[fences.length - 1] : null;
60763
+ }
60733
60764
  var FENCE_CLOSE = "\n```";
60734
60765
  function splitByLength(text, max) {
60735
60766
  const room = max - FENCE_CLOSE.length;
@@ -71905,7 +71936,6 @@ class SlackClient extends BasePlatformClient {
71905
71936
  token: this.botToken,
71906
71937
  channelId: this.channelId,
71907
71938
  allowedUsers: this.allowedUsers,
71908
- appToken: this.appToken,
71909
71939
  apiUrl: this.apiUrl,
71910
71940
  outboundFiles: this.outboundFiles,
71911
71941
  mcpServers: this.mcpServers,
@@ -12723,7 +12723,7 @@ var require_dist = __commonJS(function(exports, module) {
12723
12723
 
12724
12724
  // src/utils/emoji.ts
12725
12725
  function withoutSkinTone(emoji) {
12726
- return emoji.replace(/::skin-tone-\d$/, "");
12726
+ return emoji.replace(/::skin-tone-\d$/, "").replace(/(?<=.)_(?:light|medium_light|medium|medium_dark|dark)_skin_tone$/, "");
12727
12727
  }
12728
12728
  function isApprovalEmoji(emoji) {
12729
12729
  return APPROVAL_EMOJIS.includes(withoutSkinTone(emoji));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.39.2",
3
+ "version": "1.39.3",
4
4
  "description": "Run Claude Code from Slack or Mattermost. Sessions stream live into threads where your whole team can watch and steer.",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",