claude-threads 1.36.0 → 1.36.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 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.36.1] - 2026-09-10
11
+
12
+ ### Fixed
13
+ - **An answer given before the option emojis finish landing is no longer lost.** A question or plan-approval post goes up first, then its 1️⃣/2️⃣ (or 👍/👎) options are added one API round trip at a time. The post is already visible and reactable for that whole window, but the bot only claimed it afterwards: `MessageManager` registered the post id in the session's reaction index after `createInteractivePost` resolved, and `QuestionApprovalExecutor` recorded `currentPostId` / `pendingApproval` at the same late moment. A reaction arriving in between matched nothing and was discarded silently, with no retry and no fallback. On a modern CLI, where `AskUserQuestion` and `ExitPlanMode` block on the MCP permission prompt, the cost is not a missed click: the decision bridge stays parked and the session hangs until `MCP_TOOL_TIMEOUT` (an hour by default) on a decision the user already made. Both the post id and the executor's pending state are now claimed in a pre-reaction callback, before the first option emoji is sent. The same ordering is fixed for every reaction-gated prompt that goes through `postInteractiveAndRegister` (worktree prompts, message approvals, update prompts, bug reports, routine and watch confirmations). Whoever reacts fastest was most likely to lose their answer, and a loaded host widened the window enough to make it reproducible.
14
+ - **A reaction that beats the bot's own create response survives too.** Claiming the post id before the option emojis go out closes only the second half of the race. The platform starts accepting reactions the instant it stores the post, which is earlier still than the `POST /posts` response getting back to the bot, so on a slow or contended link a user can answer before the bot has any idea the post exists. The reaction arrives as a live websocket push and is never re-delivered, so `findByPost` dropped it for good and the session hung on the decision bridge exactly as before. A thread now announces an interactive-post create before issuing it, and a reaction on an unknown post id waits (up to 5s) for that registration instead of being discarded. The wait applies only while a create is genuinely outstanding: a reaction on any unrelated post still returns immediately, so nothing else pays for it.
15
+
10
16
  ## [1.36.0] - 2026-09-10
11
17
 
12
18
  ### Added
package/dist/index.js CHANGED
@@ -53278,15 +53278,21 @@ async function postError(session, message, addBugReaction = true) {
53278
53278
  }
53279
53279
  return result;
53280
53280
  }
53281
- async function postInteractive(session, message, reactions) {
53282
- const post = await session.platform.createInteractivePost(message, reactions, session.threadId);
53283
- updateLastMessage(session, post);
53284
- return post;
53281
+ async function postInteractive(session, message, reactions, onPostCreated) {
53282
+ const doneInFlight = typeof session.messageManager?.markInteractivePostInFlight === "function" ? session.messageManager.markInteractivePostInFlight() : () => {};
53283
+ try {
53284
+ const post = await session.platform.createInteractivePost(message, reactions, session.threadId, (created) => {
53285
+ onPostCreated?.(created);
53286
+ doneInFlight();
53287
+ });
53288
+ updateLastMessage(session, post);
53289
+ return post;
53290
+ } finally {
53291
+ doneInFlight();
53292
+ }
53285
53293
  }
53286
53294
  async function postInteractiveAndRegister(session, message, reactions, registerPost) {
53287
- const post = await postInteractive(session, message, reactions);
53288
- registerPost(post.id, session.threadId);
53289
- return post;
53295
+ return postInteractive(session, message, reactions, (created) => registerPost(created.id, session.threadId));
53290
53296
  }
53291
53297
  async function updatePost(session, postId, message) {
53292
53298
  await withErrorHandling(() => session.platform.updatePost(postId, message), { action: "Update post", session });
@@ -61156,16 +61162,26 @@ class QuestionApprovalExecutor extends BaseExecutor {
61156
61162
 
61157
61163
  ` + ctx.formatter.formatItalic("React to respond");
61158
61164
  }
61165
+ let claimed = false;
61159
61166
  const post = await ctx.createInteractivePost(message, [APPROVAL_EMOJIS[0], DENIAL_EMOJIS[0]], {
61160
61167
  type: "plan_approval",
61161
61168
  interactionType: "plan_approval",
61162
61169
  toolUseId: op.toolUseId
61170
+ }, (created) => {
61171
+ claimed = true;
61172
+ this.state.pendingApproval = {
61173
+ postId: created.id,
61174
+ type: op.approvalType,
61175
+ toolUseId: op.toolUseId
61176
+ };
61163
61177
  });
61164
- this.state.pendingApproval = {
61165
- postId: post.id,
61166
- type: op.approvalType,
61167
- toolUseId: op.toolUseId
61168
- };
61178
+ if (!claimed) {
61179
+ this.state.pendingApproval = {
61180
+ postId: post.id,
61181
+ type: op.approvalType,
61182
+ toolUseId: op.toolUseId
61183
+ };
61184
+ }
61169
61185
  ctx.logger.debug(`Created ${op.approvalType} approval post ${formatShortId(post.id)}`);
61170
61186
  }
61171
61187
  async postCurrentQuestion(ctx) {
@@ -61191,13 +61207,21 @@ class QuestionApprovalExecutor extends BaseExecutor {
61191
61207
  message += `
61192
61208
  `;
61193
61209
  }
61210
+ let claimed = false;
61194
61211
  const reactionOptions = NUMBER_EMOJIS.slice(0, q.options.length);
61195
61212
  const post = await ctx.createInteractivePost(message, reactionOptions, {
61196
61213
  type: "question",
61197
61214
  interactionType: "question",
61198
61215
  toolUseId: this.state.pendingQuestionSet.toolUseId
61216
+ }, (created) => {
61217
+ claimed = true;
61218
+ if (this.state.pendingQuestionSet) {
61219
+ this.state.pendingQuestionSet.currentPostId = created.id;
61220
+ }
61199
61221
  });
61200
- this.state.pendingQuestionSet.currentPostId = post.id;
61222
+ if (!claimed && this.state.pendingQuestionSet) {
61223
+ this.state.pendingQuestionSet.currentPostId = post.id;
61224
+ }
61201
61225
  }
61202
61226
  async handleQuestionAnswer(postId, optionIndex, ctx) {
61203
61227
  if (!this.state.pendingQuestionSet)
@@ -61881,6 +61905,7 @@ class MessageManager {
61881
61905
  worktreePath;
61882
61906
  worktreeBranch;
61883
61907
  registerPost;
61908
+ beginInteractivePost;
61884
61909
  updateLastMessage;
61885
61910
  buildMessageContentCallback;
61886
61911
  startTypingCallback;
@@ -61902,6 +61927,7 @@ class MessageManager {
61902
61927
  this.worktreePath = options.worktreePath;
61903
61928
  this.worktreeBranch = options.worktreeBranch;
61904
61929
  this.registerPost = options.registerPost;
61930
+ this.beginInteractivePost = options.beginInteractivePost;
61905
61931
  this.updateLastMessage = options.updateLastMessage;
61906
61932
  this.buildMessageContentCallback = options.buildMessageContent;
61907
61933
  this.startTypingCallback = options.startTyping;
@@ -62086,14 +62112,25 @@ class MessageManager {
62086
62112
  this.updateLastMessage(post);
62087
62113
  return post;
62088
62114
  },
62089
- createInteractivePost: async (content, reactions, options) => {
62090
- const post = await this.platform.createInteractivePost(content, reactions, this.threadId);
62091
- this.registerPost(post.id, options);
62092
- this.updateLastMessage(post);
62093
- return post;
62115
+ createInteractivePost: async (content, reactions, options, onPostCreated) => {
62116
+ const doneInFlight = this.beginInteractivePost?.(this.threadId);
62117
+ try {
62118
+ const post = await this.platform.createInteractivePost(content, reactions, this.threadId, (created) => {
62119
+ this.registerPost(created.id, options);
62120
+ doneInFlight?.();
62121
+ onPostCreated?.(created);
62122
+ });
62123
+ this.updateLastMessage(post);
62124
+ return post;
62125
+ } finally {
62126
+ doneInFlight?.();
62127
+ }
62094
62128
  }
62095
62129
  };
62096
62130
  }
62131
+ markInteractivePostInFlight() {
62132
+ return this.beginInteractivePost?.(this.threadId) ?? (() => {});
62133
+ }
62097
62134
  setWorktreeInfo(path, branch) {
62098
62135
  this.worktreePath = path;
62099
62136
  this.worktreeBranch = branch;
@@ -66727,6 +66764,60 @@ class SessionRegistry {
66727
66764
  }
66728
66765
  registerPost(postId, threadId) {
66729
66766
  this.postIndex.set(postId, threadId);
66767
+ const waiters = this.pendingPostWaiters.get(postId);
66768
+ if (waiters) {
66769
+ this.pendingPostWaiters.delete(postId);
66770
+ for (const resolve of waiters)
66771
+ resolve();
66772
+ }
66773
+ }
66774
+ inFlightInteractivePosts = new Map;
66775
+ pendingPostWaiters = new Map;
66776
+ hasInFlightInteractivePost() {
66777
+ return this.inFlightInteractivePosts.size > 0;
66778
+ }
66779
+ beginInteractivePost(threadId) {
66780
+ this.inFlightInteractivePosts.set(threadId, (this.inFlightInteractivePosts.get(threadId) ?? 0) + 1);
66781
+ let done = false;
66782
+ return () => {
66783
+ if (done)
66784
+ return;
66785
+ done = true;
66786
+ const depth = (this.inFlightInteractivePosts.get(threadId) ?? 1) - 1;
66787
+ if (depth <= 0)
66788
+ this.inFlightInteractivePosts.delete(threadId);
66789
+ else
66790
+ this.inFlightInteractivePosts.set(threadId, depth);
66791
+ };
66792
+ }
66793
+ async awaitPendingPost(postId, timeoutMs) {
66794
+ if (this.postIndex.has(postId))
66795
+ return;
66796
+ if (!this.hasInFlightInteractivePost())
66797
+ return;
66798
+ await new Promise((resolve) => {
66799
+ const waiters = this.pendingPostWaiters.get(postId) ?? [];
66800
+ let settled = false;
66801
+ const finish = () => {
66802
+ if (settled)
66803
+ return;
66804
+ settled = true;
66805
+ clearTimeout(timer);
66806
+ const list = this.pendingPostWaiters.get(postId);
66807
+ if (list) {
66808
+ const idx = list.indexOf(finish);
66809
+ if (idx >= 0)
66810
+ list.splice(idx, 1);
66811
+ if (list.length === 0)
66812
+ this.pendingPostWaiters.delete(postId);
66813
+ }
66814
+ resolve();
66815
+ };
66816
+ const timer = setTimeout(finish, timeoutMs);
66817
+ timer.unref?.();
66818
+ waiters.push(finish);
66819
+ this.pendingPostWaiters.set(postId, waiters);
66820
+ });
66730
66821
  }
66731
66822
  unregisterPost(postId) {
66732
66823
  this.postIndex.delete(postId);
@@ -67248,6 +67339,7 @@ function createMessageManager(session, ctx) {
67248
67339
  ctx.ops.registerPost(postId, session.threadId);
67249
67340
  postTracker.register(postId, session.threadId, session.sessionId, options);
67250
67341
  },
67342
+ beginInteractivePost: (threadId) => ctx.ops.beginInteractivePost(threadId),
67251
67343
  updateLastMessage: (post) => {
67252
67344
  updateLastMessage(session, post);
67253
67345
  },
@@ -69846,8 +69938,9 @@ class BasePlatformClient extends EventEmitter3 {
69846
69938
  getBotName() {
69847
69939
  return this.botName;
69848
69940
  }
69849
- async createInteractivePost(message, reactions, threadId) {
69941
+ async createInteractivePost(message, reactions, threadId, onPostCreated) {
69850
69942
  const post = await this.createPost(message, threadId);
69943
+ onPostCreated?.(post);
69851
69944
  for (const emoji of reactions) {
69852
69945
  try {
69853
69946
  await this.addReaction(post.id, emoji);
@@ -72662,6 +72755,7 @@ function shouldPostResumeRefusal(platformId, threadId, username, now = Date.now(
72662
72755
  // src/session/reaction-router.ts
72663
72756
  init_logger();
72664
72757
  var log52 = createLogger("manager");
72758
+ var UNKNOWN_POST_GRACE_MS = 5000;
72665
72759
  async function handleReaction(deps, platformId, postId, emojiName, username, action) {
72666
72760
  const normalizedEmoji = normalizeEmojiName(emojiName);
72667
72761
  if (action === "added" && isResumeEmoji(normalizedEmoji)) {
@@ -72669,9 +72763,13 @@ async function handleReaction(deps, platformId, postId, emojiName, username, act
72669
72763
  if (resumed)
72670
72764
  return;
72671
72765
  }
72672
- const session = deps.registry.findByPost(postId);
72673
- if (!session)
72674
- return;
72766
+ let session = deps.registry.findByPost(postId);
72767
+ if (!session) {
72768
+ await deps.registry.awaitPendingPost(postId, UNKNOWN_POST_GRACE_MS);
72769
+ session = deps.registry.findByPost(postId);
72770
+ if (!session)
72771
+ return;
72772
+ }
72675
72773
  if (session.platformId !== platformId)
72676
72774
  return;
72677
72775
  const ownerScoped = resolveApprovals(session.platform.approvals, isDcmThreadId(session.threadId)) === "owner";
@@ -72975,6 +73073,7 @@ class SessionManager extends EventEmitter4 {
72975
73073
  getSessionId: (pid, tid) => this.getSessionId(pid, tid),
72976
73074
  findSessionByThreadId: (tid) => this.findSessionByThreadId(tid),
72977
73075
  registerPost: (pid, tid) => this.registerPost(pid, tid),
73076
+ beginInteractivePost: (tid) => this.registry.beginInteractivePost(tid),
72978
73077
  flush: async (s) => {
72979
73078
  if (s.messageManager) {
72980
73079
  await s.messageManager.flush();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.36.0",
3
+ "version": "1.36.1",
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",