claude-threads 1.24.0 → 1.24.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,11 @@ 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.24.1] - 2026-08-08
9
+
10
+ ### Fixed
11
+ - **Task lists no longer degrade to "Task #N" placeholders after a bot restart.** Modern CLIs stream tasks incrementally (`TaskCreate`/`TaskUpdate` with ids), and the bot accumulates them in a per-session `TaskTracker` — but that tracker lived only in memory. After a restart + resume, the first `TaskUpdate` of the next turn hit an empty tracker and rendered a placeholder ("Task #1") instead of the real subject, losing every task name for the rest of the session. The tracker's resolved tasks (id → subject/status) are now persisted to `sessions.json` at each turn end and restored on resume. In-flight creates (id not yet resolved from result text) are deliberately dropped at serialize time; pre-1.24.1 persisted sessions simply start with an empty tracker as before. Covered by a red-green integration test that restarts the bot mid-task-list and asserts the post-resume re-render still shows real subjects on both platform paths.
12
+
8
13
  ## [1.24.0] - 2026-08-08
9
14
 
10
15
  ### Added
package/dist/index.js CHANGED
@@ -56301,16 +56301,23 @@ class ClaudeCli extends EventEmitter2 {
56301
56301
  const trimmed = line.trim();
56302
56302
  if (!trimmed)
56303
56303
  continue;
56304
+ let event;
56305
+ try {
56306
+ event = JSON.parse(trimmed);
56307
+ } catch {
56308
+ continue;
56309
+ }
56304
56310
  try {
56305
- const event = JSON.parse(trimmed);
56306
56311
  this.emit("event", event);
56307
- if (event.type === "result" && isErrorResultEvent(event)) {
56308
- this.maybeEmitRateLimit(trimmed);
56309
- }
56310
- if (event.type === "rate_limit_event") {
56311
- this.maybeEmitRateLimitHit(parseRateLimitEvent(event));
56312
- }
56313
- } catch {}
56312
+ } catch (err) {
56313
+ this.log.error(`'event' listener threw while handling a ${event.type} event: ${err}`);
56314
+ }
56315
+ if (event.type === "result" && isErrorResultEvent(event)) {
56316
+ this.maybeEmitRateLimit(trimmed);
56317
+ }
56318
+ if (event.type === "rate_limit_event") {
56319
+ this.maybeEmitRateLimitHit(parseRateLimitEvent(event));
56320
+ }
56314
56321
  }
56315
56322
  }
56316
56323
  maybeEmitRateLimit(text) {
@@ -65040,6 +65047,7 @@ function truncateAtWord2(text, maxLength) {
65040
65047
  return truncated + "...";
65041
65048
  }
65042
65049
  // src/operations/task-tracker.ts
65050
+ var VALID_STATUSES = new Set(["pending", "in_progress", "completed"]);
65043
65051
  var CREATED_RESULT_RE = /Task #(\S+) created/;
65044
65052
 
65045
65053
  class TaskTracker {
@@ -65133,6 +65141,38 @@ class TaskTracker {
65133
65141
  this.pendingCreates.clear();
65134
65142
  this.unmatchedCreateResults = 0;
65135
65143
  }
65144
+ serialize() {
65145
+ const restorable = this.tasks.filter((t) => Boolean(t.taskId));
65146
+ if (restorable.length === 0)
65147
+ return;
65148
+ return restorable.map((t) => {
65149
+ const out = {
65150
+ taskId: t.taskId,
65151
+ subject: t.subject,
65152
+ status: t.status
65153
+ };
65154
+ if (t.activeForm)
65155
+ out.activeForm = t.activeForm;
65156
+ if (t.isPlaceholder)
65157
+ out.isPlaceholder = true;
65158
+ return out;
65159
+ });
65160
+ }
65161
+ restore(state) {
65162
+ if (!Array.isArray(state)) {
65163
+ this.tasks = [];
65164
+ this.pendingCreates.clear();
65165
+ return;
65166
+ }
65167
+ this.tasks = state.filter((t) => t !== null && typeof t === "object" && typeof t.taskId === "string" && t.taskId.length > 0 && typeof t.subject === "string").map((t) => ({
65168
+ taskId: t.taskId,
65169
+ subject: t.subject || `Task #${t.taskId}`,
65170
+ activeForm: typeof t.activeForm === "string" ? t.activeForm : undefined,
65171
+ status: VALID_STATUSES.has(t.status) ? t.status : "pending",
65172
+ isPlaceholder: t.isPlaceholder === true ? true : undefined
65173
+ }));
65174
+ this.pendingCreates.clear();
65175
+ }
65136
65176
  }
65137
65177
 
65138
65178
  // src/operations/executors/base.ts
@@ -67227,9 +67267,15 @@ class MessageManager {
67227
67267
  serialize() {
67228
67268
  return {
67229
67269
  taskList: this.taskListExecutor.serialize(),
67270
+ taskTracker: this.taskTracker.serialize(),
67230
67271
  contextPrompt: this.promptExecutor.serialize()
67231
67272
  };
67232
67273
  }
67274
+ restoreTaskTracker(state) {
67275
+ if (state && state.length > 0) {
67276
+ this.taskTracker.restore(state);
67277
+ }
67278
+ }
67233
67279
  hydrateTaskListState(persisted) {
67234
67280
  this.taskListExecutor.hydrateState(persisted);
67235
67281
  }
@@ -68904,7 +68950,7 @@ function handleEventPreProcessing(session, event, ctx) {
68904
68950
  trackEvent(session, "tool_use", tool.name);
68905
68951
  }
68906
68952
  }
68907
- function handleEventPostProcessing(session, event, ctx) {
68953
+ function handleEventPostProcessing(session, event, ctx, mainHandling) {
68908
68954
  if (event.type === "assistant") {
68909
68955
  const msg = event.message;
68910
68956
  for (const block of msg?.content || []) {
@@ -68921,6 +68967,11 @@ function handleEventPostProcessing(session, event, ctx) {
68921
68967
  session.isProcessing = false;
68922
68968
  ctx.ops.emitSessionUpdate(session.sessionId, { status: getSessionStatus(session) });
68923
68969
  updateUsageStats(session, event, ctx);
68970
+ if (mainHandling) {
68971
+ mainHandling.catch(() => {}).then(() => ctx.ops.persistSession(session));
68972
+ } else {
68973
+ ctx.ops.persistSession(session);
68974
+ }
68924
68975
  }
68925
68976
  if (event.type === "user" && !isSidechainEvent(event)) {
68926
68977
  const msg = event.message;
@@ -70877,6 +70928,7 @@ Please start a new session.`), { action: "Post resume failure notification" });
70877
70928
  tasksCompleted: state.tasksCompleted,
70878
70929
  tasksMinimized: state.tasksMinimized
70879
70930
  });
70931
+ session.messageManager.restoreTaskTracker(state.taskTrackerState);
70880
70932
  const persistedWithInteractive = state;
70881
70933
  if (persistedWithInteractive.pendingQuestionSet || persistedWithInteractive.pendingApproval) {
70882
70934
  session.messageManager.hydrateInteractiveState({
@@ -71827,8 +71879,8 @@ class SessionManager extends EventEmitter4 {
71827
71879
  if (!session || !session.messageManager)
71828
71880
  return;
71829
71881
  handleEventPreProcessing(session, event, this.getContext());
71830
- session.messageManager.handleEvent(event);
71831
- handleEventPostProcessing(session, event, this.getContext());
71882
+ const mainHandling = session.messageManager.handleEvent(event);
71883
+ handleEventPostProcessing(session, event, this.getContext(), mainHandling);
71832
71884
  }
71833
71885
  async handleExit(sessionId, code, source) {
71834
71886
  await handleExit(sessionId, code, this.getContext(), source);
@@ -71866,11 +71918,20 @@ class SessionManager extends EventEmitter4 {
71866
71918
  this.stopTyping(session);
71867
71919
  }
71868
71920
  persistSession(session) {
71921
+ try {
71922
+ this.persistSessionUnsafe(session);
71923
+ } catch (err) {
71924
+ log35.error(`Failed to persist session ${session.sessionId}: ${err}`);
71925
+ }
71926
+ }
71927
+ persistSessionUnsafe(session) {
71869
71928
  let taskListSnapshot;
71870
71929
  let contextPromptSnapshot;
71930
+ let taskTrackerSnapshot;
71871
71931
  if (session.messageManager) {
71872
71932
  const serialized = session.messageManager.serialize();
71873
71933
  taskListSnapshot = serialized.taskList;
71934
+ taskTrackerSnapshot = serialized.taskTracker;
71874
71935
  if (serialized.contextPrompt) {
71875
71936
  contextPromptSnapshot = serialized.contextPrompt;
71876
71937
  }
@@ -71895,6 +71956,7 @@ class SessionManager extends EventEmitter4 {
71895
71956
  lastTasksContent: taskListSnapshot?.content ?? null,
71896
71957
  tasksCompleted: taskListSnapshot?.isCompleted ?? false,
71897
71958
  tasksMinimized: taskListSnapshot?.isMinimized ?? false,
71959
+ taskTrackerState: taskTrackerSnapshot,
71898
71960
  worktreeInfo: session.worktreeInfo,
71899
71961
  isWorktreeOwner: session.isWorktreeOwner,
71900
71962
  pendingWorktreePrompt: session.pendingWorktreePrompt,
@@ -49412,6 +49412,7 @@ function truncateAtWord(text, maxLength) {
49412
49412
  return truncated + "...";
49413
49413
  }
49414
49414
  // src/operations/task-tracker.ts
49415
+ var VALID_STATUSES = new Set(["pending", "in_progress", "completed"]);
49415
49416
  var CREATED_RESULT_RE = /Task #(\S+) created/;
49416
49417
 
49417
49418
  class TaskTracker {
@@ -49505,6 +49506,38 @@ class TaskTracker {
49505
49506
  this.pendingCreates.clear();
49506
49507
  this.unmatchedCreateResults = 0;
49507
49508
  }
49509
+ serialize() {
49510
+ const restorable = this.tasks.filter((t) => Boolean(t.taskId));
49511
+ if (restorable.length === 0)
49512
+ return;
49513
+ return restorable.map((t) => {
49514
+ const out = {
49515
+ taskId: t.taskId,
49516
+ subject: t.subject,
49517
+ status: t.status
49518
+ };
49519
+ if (t.activeForm)
49520
+ out.activeForm = t.activeForm;
49521
+ if (t.isPlaceholder)
49522
+ out.isPlaceholder = true;
49523
+ return out;
49524
+ });
49525
+ }
49526
+ restore(state) {
49527
+ if (!Array.isArray(state)) {
49528
+ this.tasks = [];
49529
+ this.pendingCreates.clear();
49530
+ return;
49531
+ }
49532
+ this.tasks = state.filter((t) => t !== null && typeof t === "object" && typeof t.taskId === "string" && t.taskId.length > 0 && typeof t.subject === "string").map((t) => ({
49533
+ taskId: t.taskId,
49534
+ subject: t.subject || `Task #${t.taskId}`,
49535
+ activeForm: typeof t.activeForm === "string" ? t.activeForm : undefined,
49536
+ status: VALID_STATUSES.has(t.status) ? t.status : "pending",
49537
+ isPlaceholder: t.isPlaceholder === true ? true : undefined
49538
+ }));
49539
+ this.pendingCreates.clear();
49540
+ }
49508
49541
  }
49509
49542
 
49510
49543
  // src/operations/executors/base.ts
@@ -51908,9 +51941,15 @@ class MessageManager {
51908
51941
  serialize() {
51909
51942
  return {
51910
51943
  taskList: this.taskListExecutor.serialize(),
51944
+ taskTracker: this.taskTracker.serialize(),
51911
51945
  contextPrompt: this.promptExecutor.serialize()
51912
51946
  };
51913
51947
  }
51948
+ restoreTaskTracker(state) {
51949
+ if (state && state.length > 0) {
51950
+ this.taskTracker.restore(state);
51951
+ }
51952
+ }
51914
51953
  hydrateTaskListState(persisted) {
51915
51954
  this.taskListExecutor.hydrateState(persisted);
51916
51955
  }
@@ -56409,16 +56448,23 @@ class ClaudeCli extends EventEmitter2 {
56409
56448
  const trimmed = line.trim();
56410
56449
  if (!trimmed)
56411
56450
  continue;
56451
+ let event;
56452
+ try {
56453
+ event = JSON.parse(trimmed);
56454
+ } catch {
56455
+ continue;
56456
+ }
56412
56457
  try {
56413
- const event = JSON.parse(trimmed);
56414
56458
  this.emit("event", event);
56415
- if (event.type === "result" && isErrorResultEvent(event)) {
56416
- this.maybeEmitRateLimit(trimmed);
56417
- }
56418
- if (event.type === "rate_limit_event") {
56419
- this.maybeEmitRateLimitHit(parseRateLimitEvent(event));
56420
- }
56421
- } catch {}
56459
+ } catch (err) {
56460
+ this.log.error(`'event' listener threw while handling a ${event.type} event: ${err}`);
56461
+ }
56462
+ if (event.type === "result" && isErrorResultEvent(event)) {
56463
+ this.maybeEmitRateLimit(trimmed);
56464
+ }
56465
+ if (event.type === "rate_limit_event") {
56466
+ this.maybeEmitRateLimitHit(parseRateLimitEvent(event));
56467
+ }
56422
56468
  }
56423
56469
  }
56424
56470
  maybeEmitRateLimit(text) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.24.0",
3
+ "version": "1.24.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",