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 +6 -0
- package/dist/index.js +121 -22
- package/package.json +1 -1
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
|
|
53283
|
-
|
|
53284
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61165
|
-
|
|
61166
|
-
|
|
61167
|
-
|
|
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
|
|
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
|
|
62091
|
-
|
|
62092
|
-
|
|
62093
|
-
|
|
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
|
-
|
|
72673
|
-
if (!session)
|
|
72674
|
-
|
|
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