claude-threads 1.9.0 → 1.9.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
@@ -5,6 +5,15 @@ 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.9.1] - 2026-04-24
9
+
10
+ ### Internals
11
+ - **Unified `Executor<TState>` contract.** New interface in `src/operations/executors/types.ts` formalizes what `MessageManager` actually relies on: `getState` / `reset` required, `handleReaction` / `serialize` optional. `BaseExecutor<T>` implements it. A new `contract.test.ts` iterates every executor and asserts the shape — catches drift when someone adds an executor without the required members. (#346)
12
+ - **Uniform `handleReaction` signature across all seven reaction executors** — `(postId, emoji, user, action, ctx) => Promise<boolean>`. Previously `TaskList`, `Subagent`, and `WorktreePrompt` had slightly different shapes. `MessageManager.handleReaction` now dispatches via a `reactionDispatchList()` table instead of an if/else chain. (#346)
13
+ - **`MessageManager.serialize()` aggregates executor state** for `SessionManager.persistSession`. The writer no longer reaches into individual executors via named getters (`getTaskListState()`, `getPendingContextPrompt()`). Legacy getters kept as `@deprecated` shims — they still have non-persistence consumers. (#346)
14
+ - **Byte-identical `sessions.json` guarantee.** New snapshot tests in `manager.test.ts` pin the full payload's field set and run the new and legacy (`CLAUDE_THREADS_SERIALIZE_V2=0`) paths through a parity assertion. No persisted-schema change on disk. (#346)
15
+ - **Rollback hatch:** `CLAUDE_THREADS_SERIALIZE_V2=0` falls back to the pre-refactor per-getter writer for one release. Removed in the next minor. (#346)
16
+
8
17
  ## [1.9.0] - 2026-04-24
9
18
 
10
19
  ### Added
package/dist/index.js CHANGED
@@ -58611,7 +58611,15 @@ class TaskListExecutor extends BaseExecutor {
58611
58611
  await ctx.platform.updatePost(this.state.tasksPostId, displayContent);
58612
58612
  } catch {}
58613
58613
  }
58614
- async handleReaction(postId, emoji, action, ctx) {
58614
+ serialize() {
58615
+ return {
58616
+ postId: this.state.tasksPostId,
58617
+ content: this.state.lastTasksContent,
58618
+ isMinimized: this.state.tasksMinimized,
58619
+ isCompleted: this.state.tasksCompleted
58620
+ };
58621
+ }
58622
+ async handleReaction(postId, emoji, _user, action, ctx) {
58615
58623
  ctx.logger.debug(`TaskListExecutor.handleReaction: postId=${postId.substring(0, 8)}, emoji=${emoji}, action=${action}, tasksPostId=${this.state.tasksPostId?.substring(0, 8) ?? "none"}`);
58616
58624
  if (postId !== this.state.tasksPostId) {
58617
58625
  ctx.logger.debug(`TaskListExecutor: postId does not match tasksPostId, ignoring`);
@@ -58880,7 +58888,7 @@ class SubagentExecutor extends BaseExecutor {
58880
58888
  }
58881
58889
  return false;
58882
58890
  }
58883
- async handleReaction(postId, emoji, action, ctx) {
58891
+ async handleReaction(postId, emoji, _user, action, ctx) {
58884
58892
  ctx.logger.debug(`SubagentExecutor.handleReaction: postId=${postId.substring(0, 8)}, emoji=${emoji}, action=${action}`);
58885
58893
  if (!isMinimizeToggleEmoji(emoji)) {
58886
58894
  ctx.logger.debug(`SubagentExecutor: emoji ${emoji} is not minimize toggle, ignoring`);
@@ -59420,6 +59428,9 @@ class PromptExecutor extends BaseExecutor {
59420
59428
  getPendingContextPrompt() {
59421
59429
  return this.state.pendingContextPrompt;
59422
59430
  }
59431
+ serialize() {
59432
+ return this.state.pendingContextPrompt;
59433
+ }
59423
59434
  hasPendingContextPrompt() {
59424
59435
  return this.state.pendingContextPrompt !== null;
59425
59436
  }
@@ -60782,12 +60793,12 @@ class MessageManager {
60782
60793
  await this.taskListExecutor.bumpToBottom(this.getExecutorContext());
60783
60794
  }
60784
60795
  getTaskListState() {
60785
- const state = this.taskListExecutor.getState();
60796
+ return this.taskListExecutor.serialize();
60797
+ }
60798
+ serialize() {
60786
60799
  return {
60787
- postId: state.tasksPostId,
60788
- content: state.lastTasksContent,
60789
- isMinimized: state.tasksMinimized,
60790
- isCompleted: state.tasksCompleted
60800
+ taskList: this.taskListExecutor.serialize(),
60801
+ contextPrompt: this.promptExecutor.serialize()
60791
60802
  };
60792
60803
  }
60793
60804
  hydrateTaskListState(persisted) {
@@ -60880,33 +60891,27 @@ class MessageManager {
60880
60891
  getSession() {
60881
60892
  return this.session;
60882
60893
  }
60894
+ reactionDispatchList() {
60895
+ return [
60896
+ { name: "QuestionApprovalExecutor", executor: this.questionApprovalExecutor },
60897
+ { name: "MessageApprovalExecutor", executor: this.messageApprovalExecutor },
60898
+ { name: "PromptExecutor", executor: this.promptExecutor },
60899
+ { name: "BugReportExecutor", executor: this.bugReportExecutor },
60900
+ { name: "TaskListExecutor", executor: this.taskListExecutor },
60901
+ { name: "SubagentExecutor", executor: this.subagentExecutor }
60902
+ ];
60903
+ }
60883
60904
  async handleReaction(postId, emoji, user, action) {
60884
60905
  const logger = log18.forSession(this.sessionId);
60885
60906
  const ctx = this.getExecutorContext();
60886
60907
  logger.debug(`Routing reaction: postId=${postId}, emoji=${emoji}, user=${user}, action=${action}`);
60887
- if (await this.questionApprovalExecutor.handleReaction(postId, emoji, user, action, ctx)) {
60888
- logger.debug("Reaction handled by QuestionApprovalExecutor");
60889
- return true;
60890
- }
60891
- if (await this.messageApprovalExecutor.handleReaction(postId, emoji, user, action, ctx)) {
60892
- logger.debug("Reaction handled by MessageApprovalExecutor");
60893
- return true;
60894
- }
60895
- if (await this.promptExecutor.handleReaction(postId, emoji, user, action, ctx)) {
60896
- logger.debug("Reaction handled by PromptExecutor");
60897
- return true;
60898
- }
60899
- if (await this.bugReportExecutor.handleReaction(postId, emoji, user, action, ctx)) {
60900
- logger.debug("Reaction handled by BugReportExecutor");
60901
- return true;
60902
- }
60903
- if (await this.taskListExecutor.handleReaction(postId, emoji, action, ctx)) {
60904
- logger.debug("Reaction handled by TaskListExecutor");
60905
- return true;
60906
- }
60907
- if (await this.subagentExecutor.handleReaction(postId, emoji, action, ctx)) {
60908
- logger.debug("Reaction handled by SubagentExecutor");
60909
- return true;
60908
+ for (const { name, executor } of this.reactionDispatchList()) {
60909
+ if (!executor.handleReaction)
60910
+ continue;
60911
+ if (await executor.handleReaction(postId, emoji, user, action, ctx)) {
60912
+ logger.debug(`Reaction handled by ${name}`);
60913
+ return true;
60914
+ }
60910
60915
  }
60911
60916
  logger.debug("Reaction not handled by any executor");
60912
60917
  return false;
@@ -68910,19 +68915,29 @@ class SessionManager extends EventEmitter4 {
68910
68915
  this.stopTyping(session);
68911
68916
  }
68912
68917
  persistSession(session) {
68913
- let persistedContextPrompt;
68914
- const contextPromptState = session.messageManager?.getPendingContextPrompt();
68915
- if (contextPromptState) {
68916
- persistedContextPrompt = {
68917
- postId: contextPromptState.postId,
68918
- queuedPrompt: contextPromptState.queuedPrompt,
68919
- queuedFiles: contextPromptState.queuedFiles,
68920
- threadMessageCount: contextPromptState.threadMessageCount,
68921
- createdAt: contextPromptState.createdAt,
68922
- availableOptions: contextPromptState.availableOptions
68923
- };
68918
+ const useSerializeV2 = process.env.CLAUDE_THREADS_SERIALIZE_V2 !== "0";
68919
+ let taskListSnapshot;
68920
+ let contextPromptSnapshot;
68921
+ if (useSerializeV2 && session.messageManager) {
68922
+ const serialized = session.messageManager.serialize();
68923
+ taskListSnapshot = serialized.taskList;
68924
+ if (serialized.contextPrompt) {
68925
+ contextPromptSnapshot = serialized.contextPrompt;
68926
+ }
68927
+ } else {
68928
+ const legacyPrompt = session.messageManager?.getPendingContextPrompt();
68929
+ if (legacyPrompt) {
68930
+ contextPromptSnapshot = {
68931
+ postId: legacyPrompt.postId,
68932
+ queuedPrompt: legacyPrompt.queuedPrompt,
68933
+ queuedFiles: legacyPrompt.queuedFiles,
68934
+ threadMessageCount: legacyPrompt.threadMessageCount,
68935
+ createdAt: legacyPrompt.createdAt,
68936
+ availableOptions: legacyPrompt.availableOptions
68937
+ };
68938
+ }
68939
+ taskListSnapshot = session.messageManager?.getTaskListState();
68924
68940
  }
68925
- const taskState = session.messageManager?.getTaskListState();
68926
68941
  const state = {
68927
68942
  platformId: session.platformId,
68928
68943
  threadId: session.threadId,
@@ -68937,10 +68952,10 @@ class SessionManager extends EventEmitter4 {
68937
68952
  sessionAllowedUsers: [...session.sessionAllowedUsers],
68938
68953
  forceInteractivePermissions: session.forceInteractivePermissions,
68939
68954
  sessionStartPostId: session.sessionStartPostId,
68940
- tasksPostId: taskState?.postId ?? null,
68941
- lastTasksContent: taskState?.content ?? null,
68942
- tasksCompleted: taskState?.isCompleted ?? false,
68943
- tasksMinimized: taskState?.isMinimized ?? false,
68955
+ tasksPostId: taskListSnapshot?.postId ?? null,
68956
+ lastTasksContent: taskListSnapshot?.content ?? null,
68957
+ tasksCompleted: taskListSnapshot?.isCompleted ?? false,
68958
+ tasksMinimized: taskListSnapshot?.isMinimized ?? false,
68944
68959
  worktreeInfo: session.worktreeInfo,
68945
68960
  isWorktreeOwner: session.isWorktreeOwner,
68946
68961
  pendingWorktreePrompt: session.pendingWorktreePrompt,
@@ -68948,7 +68963,7 @@ class SessionManager extends EventEmitter4 {
68948
68963
  queuedPrompt: session.queuedPrompt,
68949
68964
  queuedFiles: session.queuedFiles,
68950
68965
  firstPrompt: session.firstPrompt,
68951
- pendingContextPrompt: persistedContextPrompt,
68966
+ pendingContextPrompt: contextPromptSnapshot,
68952
68967
  needsContextPromptOnNextMessage: session.needsContextPromptOnNextMessage,
68953
68968
  lifecyclePostId: session.lifecyclePostId,
68954
68969
  isPaused: session.lifecycle.state === "paused" || session.lifecycle.state === "interrupted",
@@ -51175,7 +51175,15 @@ class TaskListExecutor extends BaseExecutor {
51175
51175
  await ctx.platform.updatePost(this.state.tasksPostId, displayContent);
51176
51176
  } catch {}
51177
51177
  }
51178
- async handleReaction(postId, emoji4, action, ctx) {
51178
+ serialize() {
51179
+ return {
51180
+ postId: this.state.tasksPostId,
51181
+ content: this.state.lastTasksContent,
51182
+ isMinimized: this.state.tasksMinimized,
51183
+ isCompleted: this.state.tasksCompleted
51184
+ };
51185
+ }
51186
+ async handleReaction(postId, emoji4, _user, action, ctx) {
51179
51187
  ctx.logger.debug(`TaskListExecutor.handleReaction: postId=${postId.substring(0, 8)}, emoji=${emoji4}, action=${action}, tasksPostId=${this.state.tasksPostId?.substring(0, 8) ?? "none"}`);
51180
51188
  if (postId !== this.state.tasksPostId) {
51181
51189
  ctx.logger.debug(`TaskListExecutor: postId does not match tasksPostId, ignoring`);
@@ -51444,7 +51452,7 @@ class SubagentExecutor extends BaseExecutor {
51444
51452
  }
51445
51453
  return false;
51446
51454
  }
51447
- async handleReaction(postId, emoji4, action, ctx) {
51455
+ async handleReaction(postId, emoji4, _user, action, ctx) {
51448
51456
  ctx.logger.debug(`SubagentExecutor.handleReaction: postId=${postId.substring(0, 8)}, emoji=${emoji4}, action=${action}`);
51449
51457
  if (!isMinimizeToggleEmoji(emoji4)) {
51450
51458
  ctx.logger.debug(`SubagentExecutor: emoji ${emoji4} is not minimize toggle, ignoring`);
@@ -51984,6 +51992,9 @@ class PromptExecutor extends BaseExecutor {
51984
51992
  getPendingContextPrompt() {
51985
51993
  return this.state.pendingContextPrompt;
51986
51994
  }
51995
+ serialize() {
51996
+ return this.state.pendingContextPrompt;
51997
+ }
51987
51998
  hasPendingContextPrompt() {
51988
51999
  return this.state.pendingContextPrompt !== null;
51989
52000
  }
@@ -52734,12 +52745,12 @@ class MessageManager {
52734
52745
  await this.taskListExecutor.bumpToBottom(this.getExecutorContext());
52735
52746
  }
52736
52747
  getTaskListState() {
52737
- const state = this.taskListExecutor.getState();
52748
+ return this.taskListExecutor.serialize();
52749
+ }
52750
+ serialize() {
52738
52751
  return {
52739
- postId: state.tasksPostId,
52740
- content: state.lastTasksContent,
52741
- isMinimized: state.tasksMinimized,
52742
- isCompleted: state.tasksCompleted
52752
+ taskList: this.taskListExecutor.serialize(),
52753
+ contextPrompt: this.promptExecutor.serialize()
52743
52754
  };
52744
52755
  }
52745
52756
  hydrateTaskListState(persisted) {
@@ -52832,33 +52843,27 @@ class MessageManager {
52832
52843
  getSession() {
52833
52844
  return this.session;
52834
52845
  }
52846
+ reactionDispatchList() {
52847
+ return [
52848
+ { name: "QuestionApprovalExecutor", executor: this.questionApprovalExecutor },
52849
+ { name: "MessageApprovalExecutor", executor: this.messageApprovalExecutor },
52850
+ { name: "PromptExecutor", executor: this.promptExecutor },
52851
+ { name: "BugReportExecutor", executor: this.bugReportExecutor },
52852
+ { name: "TaskListExecutor", executor: this.taskListExecutor },
52853
+ { name: "SubagentExecutor", executor: this.subagentExecutor }
52854
+ ];
52855
+ }
52835
52856
  async handleReaction(postId, emoji4, user, action) {
52836
52857
  const logger = log3.forSession(this.sessionId);
52837
52858
  const ctx = this.getExecutorContext();
52838
52859
  logger.debug(`Routing reaction: postId=${postId}, emoji=${emoji4}, user=${user}, action=${action}`);
52839
- if (await this.questionApprovalExecutor.handleReaction(postId, emoji4, user, action, ctx)) {
52840
- logger.debug("Reaction handled by QuestionApprovalExecutor");
52841
- return true;
52842
- }
52843
- if (await this.messageApprovalExecutor.handleReaction(postId, emoji4, user, action, ctx)) {
52844
- logger.debug("Reaction handled by MessageApprovalExecutor");
52845
- return true;
52846
- }
52847
- if (await this.promptExecutor.handleReaction(postId, emoji4, user, action, ctx)) {
52848
- logger.debug("Reaction handled by PromptExecutor");
52849
- return true;
52850
- }
52851
- if (await this.bugReportExecutor.handleReaction(postId, emoji4, user, action, ctx)) {
52852
- logger.debug("Reaction handled by BugReportExecutor");
52853
- return true;
52854
- }
52855
- if (await this.taskListExecutor.handleReaction(postId, emoji4, action, ctx)) {
52856
- logger.debug("Reaction handled by TaskListExecutor");
52857
- return true;
52858
- }
52859
- if (await this.subagentExecutor.handleReaction(postId, emoji4, action, ctx)) {
52860
- logger.debug("Reaction handled by SubagentExecutor");
52861
- return true;
52860
+ for (const { name, executor } of this.reactionDispatchList()) {
52861
+ if (!executor.handleReaction)
52862
+ continue;
52863
+ if (await executor.handleReaction(postId, emoji4, user, action, ctx)) {
52864
+ logger.debug(`Reaction handled by ${name}`);
52865
+ return true;
52866
+ }
52862
52867
  }
52863
52868
  logger.debug("Reaction not handled by any executor");
52864
52869
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
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",