claude-threads 1.20.1 → 1.21.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,39 @@ 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.21.1] - 2026-08-07
9
+
10
+ ### Fixed
11
+ - **A respawning session can no longer be torn down by its own dying Claude process.** Two stacked races made `!cd` / `!permissions` / worktree respawns flaky (seen as the recurring `!cd should restart Claude CLI` failure on main's Integration Tests): a last event flushed by the dying process ran `resetSessionActivity`, which unconditionally flipped the session's `restarting` state back to `active` — so when the old process's exit landed, `handleExit` mistook it for the current process dying and did a full session teardown mid-restart. `resetSessionActivity` now leaves `restarting`/`cancelling` states alone, `handleExit` ignores exits from a CLI instance that is no longer the session's current one (the exit event can be delivered after the respawn already swapped in the new instance), and a successful respawn transitions to `active` explicitly instead of relying on the old exit's side effect. `ClaudeCli.kill()` gained an integration-test caller trace, mirroring the existing `sendMessage` one — attributing kills is the key question when debugging these races in CI logs.
12
+ - **`checkGitHubCli` unit tests no longer spawn the real `gh` CLI.** The two subprocess probes each carry a 5s timeout, overrunning bun's 5s per-test budget on a slow runner (the flaky `checkGitHubCli` timeout on main's CI). The exec call is now injectable and the tests cover all three outcomes (installed+authenticated, not installed, not authenticated) deterministically.
13
+ - **Integration: the `!cd` confirmation wait no longer matches the user's own message.** The loose `/changed|directory|\/tmp/i` pattern matched the `!cd /tmp` command itself, ending the test while the respawn was still in flight — `afterEach`'s `killAllSessions` then raced the restart and leaked an orphaned mock CLI into the next test.
14
+
15
+ ## [1.21.0] - 2026-08-07
16
+
17
+ ### Fixed
18
+ - **Task list display works again on modern Claude CLIs.** Claude Code moved task tracking from `TodoWrite` (whole list per call) to the incremental `TaskCreate`/`TaskUpdate` tools; verified against CLI 2.1.223, `TodoWrite` is never emitted anymore, so the bot's live task list in the thread had silently gone dark. A new per-session `TaskTracker` accumulates the incremental calls (a task's real id is only revealed by its tool result, which arrives later inside a `user` event) and feeds the existing task-list pipeline. `TaskUpdate` on an unknown id (tasks created before a resume, or by a subagent) shows a placeholder rather than nothing; `status: "deleted"` removes the task. `TodoWrite` still works for older CLIs. `TaskGet`/`TaskList` (read-only queries) are hidden from chat.
19
+ - **Tool completion indicators (`↳ ✓` / `↳ ❌ Error`) fire again.** The real CLI delivers tool results as `tool_result` blocks inside `user` events; the bot only handled a legacy top-level `tool_result` event shape that modern CLIs never emit — so per-tool completion/error indicators, elapsed times, and the tool-completion flush never triggered. The transformer now processes `user` events. Indicators are only emitted for tools that were actually displayed (hidden tools like `TaskCreate` no longer risk orphaned indicators), and elapsed time works on the real event flow (start times were previously only recorded on the legacy shape). Bug-report context tracking (tool uses/errors) was extended to the real event shapes; the legacy shapes remain handled for old captures and test fixtures.
20
+ - **Questions no longer trigger a duplicate permission prompt.** On modern CLIs `AskUserQuestion` routes through the `--permission-prompt-tool`, so next to the bot's proper question UI a generic "Permission requested: AskUserQuestion 👍✅👎" post appeared — and approving it just resolved the tool as unanswered. The MCP permission server now auto-allows `AskUserQuestion`; the question UI and reaction-answer flow are unchanged. Known limitation, verified on 2.1.223: with `--dangerously-skip-permissions` (bypass mode) the CLI does not expose `AskUserQuestion` at all, so bypass sessions cannot receive interactive questions — that is CLI behavior, not a bot regression. Also relevant: since CLI 2.1.200 an unanswered question no longer auto-continues after an idle timeout, which suits chat (users react late) — the previous auto-continue could race a slow reaction.
21
+
22
+ ### Added
23
+ - **Structured rate-limit detection.** Modern CLIs emit a `rate_limit_event` with `rate_limit_info: {status, resetsAt, rateLimitType}` on every turn. The bot now feeds **`status: "rejected"`** events into the existing account-cooldown path (`resetsAt` epoch-seconds, sanity-bounded to at most 8 days out; slightly-past values from clock skew clamp to a brief cooldown), complementing the stderr phrase-scraping which remains for older CLIs. The predicate is strict equality on `rejected` — the SDK's status union is `allowed | allowed_warning | rejected`, and `allowed_warning` (~70%+ utilization, request went through) must not bench a healthy account: cooldowns only ever extend, so one warning would have parked the account until its weekly reset.
24
+ - **The MCP plan-approval prompt now shows the plan.** On modern CLIs `ExitPlanMode` routes through the permission prompt; it used to render as a bare "Permission requested: ExitPlanMode" with no plan content. The prompt now carries the plan text (truncated at 1500 code points, with an unclosed code fence re-balanced so the reaction legend stays outside it; untruncated plans render exactly as authored). Known remaining conflict, left as follow-up work: the MCP prompt and the bot's plan-approval UI are two competing approval surfaces — reacting on the bot's UI alone lets the MCP prompt time out and deny the plan.
25
+
26
+ ### Hardening
27
+ Three adversarial review rounds ran over this change; everything they confirmed is fixed here, with red-green tests.
28
+
29
+ - **A resumed session can no longer lose its restored task list.** Task state is in-memory, so after a bot restart the resumed CLI session references task ids the fresh tracker has never seen. A single post-resume `TaskUpdate(completed)` created one completed placeholder, `allCompleted` fired on it, and the executor deleted the correctly-restored task-list post while reporting full completion. `allCompleted` now requires at least one task the tracker actually saw created, and a failed-create refresh on an empty tracker emits `update` (never `complete`) for the same reason. (Persisting tracker state across restarts, which would also restore full names instead of `Task #N` placeholders, is left as follow-up work.)
30
+ - **Fresh-CLI restarts clear accumulated task state — safely.** `!cd` and worktree switches respawn Claude as a fresh session whose task numbering restarts at #1; stale `TaskTracker` entries would collide with the new ids and corrupt updates. `MessageManager.clearClaudeSessionState()` now runs on every `resume: false` respawn (resume restarts like `!permissions` keep their state). The respawn sites await the old process before clearing, and `kill()` was reworked to resolve on stdio close (late-buffered stream-json can arrive after `exit`) with an escalation to SIGKILL and a bounded timeout — previously a failed spawn or a SIGTERM-immune process could leave `kill()` pending forever, freezing the session in `restarting`.
31
+ - **Failed `TaskCreate` calls no longer leave ghost tasks.** A create whose tool result is an error (or doesn't carry the expected "Task #N created" text — e.g. a future CLI rewording) removes the task and refreshes the display; previously it stayed forever as an un-updatable pending row that also pinned `allCompleted` at false. A non-error result with unexpected wording additionally logs a warning — that wording is the one CLI dependency whose drift would silently darken the task display again.
32
+ - **Sidechain events are filtered everywhere.** Events carrying `parent_tool_use_id` (subagent activity forwarded by some CLI versions) no longer reach the transformer — a subagent's `TaskCreate`/`TaskUpdate` calls would otherwise permanently pollute the main thread's task list with colliding ids — and the event handlers apply the same filter: subagent text no longer executes `!cd`-style Claude commands against the main session, and subagent tool uses/errors no longer pollute bug-report context. PR-URL detection intentionally still sees subagent text.
33
+ - **Task-list updates are coalesced per event.** A parallel burst of N `TaskCreate` calls in one assistant event — or N of their results failing in one `user` event — now yields one task-post update (the final snapshot) instead of N back-to-back `updatePost` calls that could trip platform rate limits.
34
+ - **Rate-limit cooldown precision.** A reset-less hit (the 1-hour default guess) is ignored while a cooldown from an explicit reset time is running, so the guess can't stretch a known-shorter deadline — but reset-less repeats during a guess-based cooldown still extend it (the documented behavior), and an explicit reset that arrives too late to re-emit still records its explicitness so later guesses defer to it regardless of arrival order.
35
+ - **Placeholder merge on late id resolution.** If a `TaskUpdate` for id N arrives before the corresponding create's result resolves that id, the placeholder and the real task are merged into one row (adopting the update's status and any rename) and the display refreshes immediately.
36
+ - **Tracker robustness.** Numeric `taskId` values are coerced instead of silently freezing the displayed list; multi-MB tool results are no longer copied when no `TaskCreate` is pending; and a session mixing both task dialects no longer flip-flops — the full-list `TodoWrite` clears the incremental tracker.
37
+
38
+ ### Changed
39
+ - **Verified against Claude CLI 2.1.223** (previously 2.1.116); suggested install version in error messages and onboarding updated. The compatible range is unchanged (`>=2.0.74 <2.2.0`).
40
+
8
41
  ## [1.20.1] - 2026-08-06
9
42
 
10
43
  ### Added
package/dist/index.js CHANGED
@@ -51448,7 +51448,7 @@ function validateClaudeCli() {
51448
51448
  version: result.version,
51449
51449
  compatible: false,
51450
51450
  message: `Claude CLI version ${result.version} is not compatible. Required: ${CLAUDE_CLI_VERSION_RANGE}
51451
- ` + `Install a compatible version: npm install -g @anthropic-ai/claude-code@2.1.116`,
51451
+ ` + `Install a compatible version: npm install -g @anthropic-ai/claude-code@2.1.223`,
51452
51452
  rawOutput: result.rawOutput ?? undefined
51453
51453
  };
51454
51454
  }
@@ -51698,7 +51698,7 @@ async function runOnboarding(reconfigure = false) {
51698
51698
  console.log(dim(` ⚠️ Claude Code CLI ${claudeCheck.version} is not compatible`));
51699
51699
  console.log("");
51700
51700
  console.log(dim(` Install a compatible version:`));
51701
- console.log(dim(" npm install -g @anthropic-ai/claude-code@2.1.1"));
51701
+ console.log(dim(" npm install -g @anthropic-ai/claude-code@2.1.223"));
51702
51702
  } else {
51703
51703
  console.log(dim(" ⚠️ Claude Code CLI found but version could not be determined"));
51704
51704
  if (claudeCheck.rawOutput) {
@@ -55918,6 +55918,25 @@ function extractResetAt(text, now) {
55918
55918
  }
55919
55919
  return;
55920
55920
  }
55921
+ function parseRateLimitEvent(event, now = Date.now()) {
55922
+ const info = event?.rate_limit_info;
55923
+ if (!info || typeof info !== "object")
55924
+ return { detected: false };
55925
+ const { status, resetsAt } = info;
55926
+ if (status !== "rejected")
55927
+ return { detected: false };
55928
+ const PAST_SKEW_TOLERANCE_MS = 2 * 60000;
55929
+ let resetAtEpochMs;
55930
+ if (typeof resetsAt === "number") {
55931
+ const ms = resetsAt * 1000;
55932
+ if (ms > now && ms - now < 8 * 86400000) {
55933
+ resetAtEpochMs = ms;
55934
+ } else if (ms <= now && now - ms < PAST_SKEW_TOLERANCE_MS) {
55935
+ resetAtEpochMs = now + 60000;
55936
+ }
55937
+ }
55938
+ return { detected: true, matched: `rate_limit_event status=${status}`, resetAtEpochMs };
55939
+ }
55921
55940
 
55922
55941
  // src/claude/cli.ts
55923
55942
  var log8 = createLogger("claude");
@@ -56054,6 +56073,7 @@ class ClaudeCli extends EventEmitter2 {
56054
56073
  stderrBuffer = "";
56055
56074
  mcpConfigTempFile = null;
56056
56075
  lastEmittedRateLimitDeadline = 0;
56076
+ lastEmittedHitHadExplicitReset = false;
56057
56077
  log;
56058
56078
  constructor(options) {
56059
56079
  super();
@@ -56107,6 +56127,7 @@ class ClaudeCli extends EventEmitter2 {
56107
56127
  totalStderrBytes -= this.stderrBuffer.length;
56108
56128
  this.stderrBuffer = "";
56109
56129
  this.lastEmittedRateLimitDeadline = 0;
56130
+ this.lastEmittedHitHadExplicitReset = false;
56110
56131
  cleanupBrowserBridgeSockets();
56111
56132
  const claudePath = getClaudePath();
56112
56133
  const args = [
@@ -56256,18 +56277,31 @@ class ClaudeCli extends EventEmitter2 {
56256
56277
  if (event.type === "result" && isErrorResultEvent(event)) {
56257
56278
  this.maybeEmitRateLimit(trimmed);
56258
56279
  }
56280
+ if (event.type === "rate_limit_event") {
56281
+ this.maybeEmitRateLimitHit(parseRateLimitEvent(event));
56282
+ }
56259
56283
  } catch {}
56260
56284
  }
56261
56285
  }
56262
56286
  maybeEmitRateLimit(text) {
56263
- const hit = detectRateLimit(text);
56287
+ this.maybeEmitRateLimitHit(detectRateLimit(text));
56288
+ }
56289
+ maybeEmitRateLimitHit(hit) {
56264
56290
  if (!hit.detected)
56265
56291
  return;
56292
+ if (!hit.resetAtEpochMs && this.lastEmittedHitHadExplicitReset && this.lastEmittedRateLimitDeadline > Date.now()) {
56293
+ return;
56294
+ }
56266
56295
  const newDeadline = cooldownDeadline(hit);
56267
56296
  const MIN_ADVANCE_MS = 60000;
56268
- if (newDeadline - this.lastEmittedRateLimitDeadline < MIN_ADVANCE_MS)
56297
+ if (newDeadline - this.lastEmittedRateLimitDeadline < MIN_ADVANCE_MS) {
56298
+ if (hit.resetAtEpochMs !== undefined) {
56299
+ this.lastEmittedHitHadExplicitReset = true;
56300
+ }
56269
56301
  return;
56302
+ }
56270
56303
  this.lastEmittedRateLimitDeadline = newDeadline;
56304
+ this.lastEmittedHitHadExplicitReset = hit.resetAtEpochMs !== undefined;
56271
56305
  this.log.warn(`Rate limit detected: ${hit.matched ?? "(no match text)"}`);
56272
56306
  this.emit("rate-limit", hit);
56273
56307
  }
@@ -56307,6 +56341,12 @@ class ClaudeCli extends EventEmitter2 {
56307
56341
  const pid = proc.pid;
56308
56342
  this.process = null;
56309
56343
  this.log.debug(`Killing Claude process (pid=${pid})`);
56344
+ if (process.env.INTEGRATION_TEST === "1") {
56345
+ const stack = new Error().stack?.split(`
56346
+ `).slice(2, 7).join(" > ").replace(/\s+at\s+/g, " < ") ?? "?";
56347
+ process.stderr.write(`[claude-cli kill pid=${pid}] | ${stack}
56348
+ `);
56349
+ }
56310
56350
  return new Promise((resolve4) => {
56311
56351
  this.log.debug("Sending first SIGINT");
56312
56352
  proc.kill("SIGINT");
@@ -56322,12 +56362,22 @@ class ClaudeCli extends EventEmitter2 {
56322
56362
  proc.kill("SIGTERM");
56323
56363
  } catch {}
56324
56364
  }, 2000);
56325
- proc.once("exit", (code) => {
56326
- this.log.debug(`Claude process exited (code=${code})`);
56365
+ const settle = (reason) => {
56366
+ this.log.debug(`Claude process gone (${reason})`);
56327
56367
  clearTimeout(secondSigint);
56328
56368
  clearTimeout(forceKillTimeout);
56369
+ clearTimeout(lastResort);
56329
56370
  resolve4();
56330
- });
56371
+ };
56372
+ const lastResort = setTimeout(() => {
56373
+ try {
56374
+ this.log.warn("Claude process did not exit after SIGTERM — sending SIGKILL");
56375
+ proc.kill("SIGKILL");
56376
+ } catch {}
56377
+ settle("kill timeout");
56378
+ }, 5000);
56379
+ proc.once("close", (code) => settle(`closed, code=${code}`));
56380
+ proc.once("error", () => settle("spawn error"));
56331
56381
  });
56332
56382
  }
56333
56383
  interrupt() {
@@ -58482,7 +58532,10 @@ function resetSessionActivity(session) {
58482
58532
  session.lastActivityAt = new Date;
58483
58533
  session.timeoutWarningPosted = false;
58484
58534
  session.lifecyclePostId = undefined;
58485
- transitionTo(session, "active");
58535
+ const state = session.lifecycle.state;
58536
+ if (state !== "restarting" && state !== "cancelling") {
58537
+ transitionTo(session, "active");
58538
+ }
58486
58539
  if (session.worktreeInfo?.worktreePath) {
58487
58540
  updateWorktreeActivity(session.worktreeInfo.worktreePath, session.sessionId);
58488
58541
  }
@@ -62597,9 +62650,9 @@ _Reported via claude-threads bug report feature_`);
62597
62650
  function escapeShell(str2) {
62598
62651
  return str2.replace(/"/g, "\\\"");
62599
62652
  }
62600
- function checkGitHubCli() {
62653
+ function checkGitHubCli(exec2 = execSync2) {
62601
62654
  try {
62602
- execSync2("gh --version", {
62655
+ exec2("gh --version", {
62603
62656
  encoding: "utf-8",
62604
62657
  timeout: 5000,
62605
62658
  stdio: ["pipe", "pipe", "pipe"]
@@ -62612,7 +62665,7 @@ function checkGitHubCli() {
62612
62665
  };
62613
62666
  }
62614
62667
  try {
62615
- execSync2("gh auth status", {
62668
+ exec2("gh auth status", {
62616
62669
  encoding: "utf-8",
62617
62670
  timeout: 5000,
62618
62671
  stdio: ["pipe", "pipe", "pipe"]
@@ -63986,11 +64039,26 @@ var bashToolFormatter = {
63986
64039
  };
63987
64040
  // src/operations/tool-formatters/task-tools.ts
63988
64041
  var taskToolsFormatter = {
63989
- toolNames: ["TodoWrite", "Task", "EnterPlanMode", "ExitPlanMode", "AskUserQuestion"],
63990
- format(toolName, _input, options) {
64042
+ toolNames: [
64043
+ "TodoWrite",
64044
+ "TaskCreate",
64045
+ "TaskUpdate",
64046
+ "TaskGet",
64047
+ "TaskList",
64048
+ "Task",
64049
+ "EnterPlanMode",
64050
+ "ExitPlanMode",
64051
+ "AskUserQuestion"
64052
+ ],
64053
+ format(toolName, input, options) {
63991
64054
  const { formatter } = options;
63992
64055
  switch (toolName) {
63993
64056
  case "TodoWrite":
64057
+ case "TaskCreate":
64058
+ case "TaskUpdate":
64059
+ return { display: null, hidden: true };
64060
+ case "TaskGet":
64061
+ case "TaskList":
63994
64062
  return { display: null, hidden: true };
63995
64063
  case "Task":
63996
64064
  return { display: null, hidden: true };
@@ -63999,8 +64067,25 @@ var taskToolsFormatter = {
63999
64067
  display: `\uD83D\uDCCB ${formatter.formatBold("Planning...")}`,
64000
64068
  permissionText: `\uD83D\uDCCB ${formatter.formatBold("Planning...")}`
64001
64069
  };
64002
- case "ExitPlanMode":
64003
- return { display: null, hidden: true };
64070
+ case "ExitPlanMode": {
64071
+ const plan = typeof input.plan === "string" ? input.plan : "";
64072
+ const points = Array.from(plan);
64073
+ const truncated = points.length > 1500;
64074
+ let preview = truncated ? `${points.slice(0, 1500).join("")}
64075
+ …` : plan;
64076
+ if (truncated) {
64077
+ const fenceCount = (preview.match(/^```/gm) || []).length;
64078
+ if (fenceCount % 2 === 1)
64079
+ preview += "\n```";
64080
+ }
64081
+ return {
64082
+ display: null,
64083
+ hidden: true,
64084
+ permissionText: preview ? `\uD83D\uDCCB ${formatter.formatBold("Plan approval requested")}
64085
+
64086
+ ${preview}` : `\uD83D\uDCCB ${formatter.formatBold("Plan approval requested")}`
64087
+ };
64088
+ }
64004
64089
  case "AskUserQuestion":
64005
64090
  return { display: null, hidden: true };
64006
64091
  default:
@@ -64539,9 +64624,14 @@ function createStatusUpdateOp(sessionId, options) {
64539
64624
  }
64540
64625
  // src/operations/transformer.ts
64541
64626
  function transformEvent(event, ctx) {
64627
+ if (event.parent_tool_use_id) {
64628
+ return [];
64629
+ }
64542
64630
  switch (event.type) {
64543
64631
  case "assistant":
64544
64632
  return transformAssistant(event, ctx);
64633
+ case "user":
64634
+ return transformUser(event, ctx);
64545
64635
  case "tool_use":
64546
64636
  return transformToolUse(event, ctx);
64547
64637
  case "tool_result":
@@ -64583,6 +64673,9 @@ function transformAssistant(event, ctx) {
64583
64673
  if (result.display && !result.hidden) {
64584
64674
  flushTextBuffer();
64585
64675
  operations.push(createAppendContentOp(ctx.sessionId, result.display, true));
64676
+ if (block.id) {
64677
+ ctx.toolStartTimes.set(block.id, Date.now());
64678
+ }
64586
64679
  }
64587
64680
  }
64588
64681
  } else if (block.type === "thinking" && block.thinking) {
@@ -64597,8 +64690,70 @@ function transformAssistant(event, ctx) {
64597
64690
  }
64598
64691
  }
64599
64692
  flushTextBuffer();
64693
+ const taskListOps = operations.filter((op) => op.type === "task_list");
64694
+ if (taskListOps.length > 1) {
64695
+ const last = taskListOps[taskListOps.length - 1];
64696
+ return operations.filter((op) => op.type !== "task_list" || op === last);
64697
+ }
64698
+ return operations;
64699
+ }
64700
+ function transformUser(event, ctx) {
64701
+ const msg = event.message;
64702
+ if (!Array.isArray(msg?.content)) {
64703
+ return [];
64704
+ }
64705
+ const operations = [];
64706
+ let indicatorCount = 0;
64707
+ for (const block of msg.content) {
64708
+ if (block?.type !== "tool_result" || !block.tool_use_id)
64709
+ continue;
64710
+ if (ctx.taskTracker.hasPendingCreate(block.tool_use_id)) {
64711
+ const resolution = ctx.taskTracker.resolveCreatedId(block.tool_use_id, toolResultContentText(block.content), block.is_error === true);
64712
+ if (resolution === "removed" || resolution === "merged") {
64713
+ const action = ctx.taskTracker.allCompleted ? "complete" : "update";
64714
+ operations.push(createTaskListOp(ctx.sessionId, action, ctx.taskTracker.toTaskItems()));
64715
+ }
64716
+ }
64717
+ if (ctx.toolStartTimes.has(block.tool_use_id)) {
64718
+ operations.push(createResultIndicatorOp(block.tool_use_id, block.is_error === true, ctx));
64719
+ indicatorCount++;
64720
+ }
64721
+ }
64722
+ const taskListOps = operations.filter((op) => op.type === "task_list");
64723
+ if (taskListOps.length > 1) {
64724
+ const last = taskListOps[taskListOps.length - 1];
64725
+ const coalesced = operations.filter((op) => op.type !== "task_list" || op === last);
64726
+ operations.length = 0;
64727
+ operations.push(...coalesced);
64728
+ }
64729
+ if (indicatorCount > 0) {
64730
+ operations.push(createFlushOp(ctx.sessionId, "tool_complete"));
64731
+ }
64600
64732
  return operations;
64601
64733
  }
64734
+ function toolResultContentText(content) {
64735
+ if (typeof content === "string")
64736
+ return content;
64737
+ if (Array.isArray(content)) {
64738
+ return content.map((b) => b && typeof b === "object" && typeof b.text === "string" ? b.text : "").join(`
64739
+ `);
64740
+ }
64741
+ return "";
64742
+ }
64743
+ function createResultIndicatorOp(toolUseId, isError, ctx) {
64744
+ let elapsed = "";
64745
+ const startTime = ctx.toolStartTimes.get(toolUseId);
64746
+ if (startTime) {
64747
+ const secs = Math.round((Date.now() - startTime) / 1000);
64748
+ if (secs >= 3) {
64749
+ elapsed = ` (${secs}s)`;
64750
+ }
64751
+ ctx.toolStartTimes.delete(toolUseId);
64752
+ }
64753
+ const icon = isError ? "❌" : "✓";
64754
+ const errorNote = isError ? " Error" : "";
64755
+ return createAppendContentOp(ctx.sessionId, ` ↳ ${icon}${errorNote}${elapsed}`, true);
64756
+ }
64602
64757
  function transformToolUse(event, ctx) {
64603
64758
  const tool = event.tool_use;
64604
64759
  if (tool.id) {
@@ -64623,23 +64778,10 @@ function transformToolResult(event, ctx) {
64623
64778
  return [];
64624
64779
  }
64625
64780
  const result = event.tool_result;
64626
- const operations = [];
64627
- let elapsed = "";
64628
- if (result.tool_use_id) {
64629
- const startTime = ctx.toolStartTimes.get(result.tool_use_id);
64630
- if (startTime) {
64631
- const secs = Math.round((Date.now() - startTime) / 1000);
64632
- if (secs >= 3) {
64633
- elapsed = ` (${secs}s)`;
64634
- }
64635
- ctx.toolStartTimes.delete(result.tool_use_id);
64636
- }
64637
- }
64638
- const icon = result.is_error ? "❌" : "✓";
64639
- const errorNote = result.is_error ? " Error" : "";
64640
- operations.push(createAppendContentOp(ctx.sessionId, ` ↳ ${icon}${errorNote}${elapsed}`, true));
64641
- operations.push(createFlushOp(ctx.sessionId, "tool_complete"));
64642
- return operations;
64781
+ return [
64782
+ createResultIndicatorOp(result.tool_use_id || "", result.is_error === true, ctx),
64783
+ createFlushOp(ctx.sessionId, "tool_complete")
64784
+ ];
64643
64785
  }
64644
64786
  function transformResult(event, ctx) {
64645
64787
  const operations = [];
@@ -64656,6 +64798,10 @@ function handleSpecialTool(toolName, toolUseId, input, ctx) {
64656
64798
  switch (toolName) {
64657
64799
  case "TodoWrite":
64658
64800
  return handleTodoWrite(input, ctx);
64801
+ case "TaskCreate":
64802
+ return handleTaskCreate(toolUseId, input, ctx);
64803
+ case "TaskUpdate":
64804
+ return handleTaskUpdate(input, ctx);
64659
64805
  case "Task":
64660
64806
  return handleTaskStart(toolUseId, input, ctx);
64661
64807
  case "AskUserQuestion":
@@ -64673,10 +64819,22 @@ function handleTodoWrite(input, ctx) {
64673
64819
  status: t.status,
64674
64820
  activeForm: t.activeForm
64675
64821
  }));
64822
+ ctx.taskTracker.clear();
64676
64823
  const allCompleted = tasks.every((t) => t.status === "completed");
64677
64824
  const action = allCompleted ? "complete" : "update";
64678
64825
  return [createTaskListOp(ctx.sessionId, action, tasks)];
64679
64826
  }
64827
+ function handleTaskCreate(toolUseId, input, ctx) {
64828
+ ctx.taskTracker.create(toolUseId, input);
64829
+ return [createTaskListOp(ctx.sessionId, "update", ctx.taskTracker.toTaskItems())];
64830
+ }
64831
+ function handleTaskUpdate(input, ctx) {
64832
+ const changed = ctx.taskTracker.update(input);
64833
+ if (!changed)
64834
+ return [];
64835
+ const action = ctx.taskTracker.allCompleted ? "complete" : "update";
64836
+ return [createTaskListOp(ctx.sessionId, action, ctx.taskTracker.toTaskItems())];
64837
+ }
64680
64838
  function handleTaskStart(toolUseId, input, ctx) {
64681
64839
  const description = input.description || input.prompt || "Subagent";
64682
64840
  const subagentType = input.subagent_type || "general-purpose";
@@ -64710,6 +64868,102 @@ function truncateAtWord2(text, maxLength) {
64710
64868
  }
64711
64869
  return truncated + "...";
64712
64870
  }
64871
+ // src/operations/task-tracker.ts
64872
+ var CREATED_RESULT_RE = /Task #(\S+) created/;
64873
+
64874
+ class TaskTracker {
64875
+ tasks = [];
64876
+ pendingCreates = new Map;
64877
+ unmatchedCreateResults = 0;
64878
+ create(toolUseId, input) {
64879
+ const task = {
64880
+ subject: typeof input.subject === "string" ? input.subject : "Task",
64881
+ activeForm: typeof input.activeForm === "string" ? input.activeForm : undefined,
64882
+ status: "pending"
64883
+ };
64884
+ this.tasks.push(task);
64885
+ if (toolUseId) {
64886
+ this.pendingCreates.set(toolUseId, task);
64887
+ }
64888
+ }
64889
+ hasPendingCreate(toolUseId) {
64890
+ return this.pendingCreates.has(toolUseId);
64891
+ }
64892
+ resolveCreatedId(toolUseId, resultContent, isError = false) {
64893
+ const task = this.pendingCreates.get(toolUseId);
64894
+ if (!task)
64895
+ return "ignored";
64896
+ this.pendingCreates.delete(toolUseId);
64897
+ const match = isError ? null : CREATED_RESULT_RE.exec(resultContent);
64898
+ if (!match) {
64899
+ if (!isError)
64900
+ this.unmatchedCreateResults++;
64901
+ this.tasks = this.tasks.filter((t) => t !== task);
64902
+ return "removed";
64903
+ }
64904
+ const placeholder = this.tasks.find((t) => t !== task && t.taskId === match[1]);
64905
+ if (placeholder) {
64906
+ task.status = placeholder.status;
64907
+ if (placeholder.subject !== `Task #${match[1]}`)
64908
+ task.subject = placeholder.subject;
64909
+ if (placeholder.activeForm && !task.activeForm)
64910
+ task.activeForm = placeholder.activeForm;
64911
+ this.tasks = this.tasks.filter((t) => t !== placeholder);
64912
+ }
64913
+ task.taskId = match[1];
64914
+ return placeholder ? "merged" : "resolved";
64915
+ }
64916
+ consumeUnmatchedCreateResultFlag() {
64917
+ if (this.unmatchedCreateResults === 0)
64918
+ return false;
64919
+ this.unmatchedCreateResults = 0;
64920
+ return true;
64921
+ }
64922
+ update(input) {
64923
+ const taskId = typeof input.taskId === "string" ? input.taskId : typeof input.taskId === "number" ? String(input.taskId) : undefined;
64924
+ if (!taskId)
64925
+ return false;
64926
+ let task = this.tasks.find((t) => t.taskId === taskId);
64927
+ const status = typeof input.status === "string" ? input.status : undefined;
64928
+ if (status === "deleted") {
64929
+ if (!task)
64930
+ return false;
64931
+ this.tasks = this.tasks.filter((t) => t !== task);
64932
+ return true;
64933
+ }
64934
+ if (!task) {
64935
+ task = { taskId, subject: `Task #${taskId}`, status: "pending", isPlaceholder: true };
64936
+ this.tasks.push(task);
64937
+ }
64938
+ if (typeof input.subject === "string")
64939
+ task.subject = input.subject;
64940
+ if (typeof input.activeForm === "string")
64941
+ task.activeForm = input.activeForm;
64942
+ if (status === "pending" || status === "in_progress" || status === "completed") {
64943
+ task.status = status;
64944
+ }
64945
+ return true;
64946
+ }
64947
+ toTaskItems() {
64948
+ return this.tasks.map((t) => ({
64949
+ content: t.subject,
64950
+ status: t.status,
64951
+ activeForm: t.activeForm ?? t.subject
64952
+ }));
64953
+ }
64954
+ get isEmpty() {
64955
+ return this.tasks.length === 0;
64956
+ }
64957
+ get allCompleted() {
64958
+ return this.tasks.length > 0 && this.tasks.every((t) => t.status === "completed") && this.tasks.some((t) => !t.isPlaceholder);
64959
+ }
64960
+ clear() {
64961
+ this.tasks = [];
64962
+ this.pendingCreates.clear();
64963
+ this.unmatchedCreateResults = 0;
64964
+ }
64965
+ }
64966
+
64713
64967
  // src/operations/executors/base.ts
64714
64968
  class BaseExecutor {
64715
64969
  state;
@@ -66462,6 +66716,7 @@ class MessageManager {
66462
66716
  startTypingCallback;
66463
66717
  emitSessionUpdateCallback;
66464
66718
  toolStartTimes = new Map;
66719
+ taskTracker = new TaskTracker;
66465
66720
  flushTimer = null;
66466
66721
  static DEFAULT_FLUSH_DELAY_MS = 500;
66467
66722
  flushDelayMs;
@@ -66532,10 +66787,14 @@ class MessageManager {
66532
66787
  sessionId: this.sessionId,
66533
66788
  formatter: this.platform.getFormatter(),
66534
66789
  toolStartTimes: this.toolStartTimes,
66790
+ taskTracker: this.taskTracker,
66535
66791
  detailed: true,
66536
66792
  worktreeInfo: this.worktreePath && this.worktreeBranch ? { path: this.worktreePath, branch: this.worktreeBranch } : undefined
66537
66793
  };
66538
66794
  const ops = transformEvent(event, transformCtx);
66795
+ if (this.taskTracker.consumeUnmatchedCreateResultFlag()) {
66796
+ logger.warn('TaskCreate result did not match the expected "Task #N created" wording — ' + "task dropped from the displayed list. If this repeats, the Claude CLI " + "likely changed its result text and the task display will be incomplete.");
66797
+ }
66539
66798
  if (ops.length === 0) {
66540
66799
  if (event.type !== "system") {
66541
66800
  logger.debug(`No operations from event: ${event.type}`);
@@ -66919,9 +67178,14 @@ class MessageManager {
66919
67178
  logger.debug("Reaction not handled by any executor");
66920
67179
  return false;
66921
67180
  }
67181
+ clearClaudeSessionState() {
67182
+ this.toolStartTimes.clear();
67183
+ this.taskTracker.clear();
67184
+ }
66922
67185
  reset() {
66923
67186
  this.cancelScheduledFlush();
66924
67187
  this.toolStartTimes.clear();
67188
+ this.taskTracker.clear();
66925
67189
  this.contentExecutor.reset();
66926
67190
  this.taskListExecutor.reset();
66927
67191
  this.questionApprovalExecutor.reset();
@@ -67957,7 +68221,7 @@ async function createAndSwitchToWorktree(session, branch, username, options) {
67957
68221
  if (session.claude.isRunning()) {
67958
68222
  options.stopTyping(session);
67959
68223
  transitionTo(session, "restarting");
67960
- session.claude.kill();
68224
+ await session.claude.kill();
67961
68225
  await options.flush(session);
67962
68226
  const newSessionId = randomUUID2();
67963
68227
  session.claudeSessionId = newSessionId;
@@ -67977,10 +68241,15 @@ async function createAndSwitchToWorktree(session, branch, username, options) {
67977
68241
  resume: false,
67978
68242
  appendSystemPrompt: await buildAppendSystemPrompt(session.platform, session.platformId, existing.path, session.threadId, session.startedBy, session.sessionAllowedUsers, options.appendSystemPrompt ?? "", options.githubEmailsStore, { omitSessionContext: !needsTitlePrompt, userAttribution: session.userAttribution })
67979
68243
  };
67980
- session.claude = new ClaudeCli(cliOptions);
67981
- session.claude.on("event", (e) => options.handleEvent(session.sessionId, e));
67982
- session.claude.on("exit", (code) => options.handleExit(session.sessionId, code));
67983
- session.claude.start();
68244
+ session.messageManager?.clearClaudeSessionState();
68245
+ const newClaude = new ClaudeCli(cliOptions);
68246
+ session.claude = newClaude;
68247
+ newClaude.on("event", (e) => options.handleEvent(session.sessionId, e));
68248
+ newClaude.on("exit", (code) => options.handleExit(session.sessionId, code, newClaude));
68249
+ newClaude.start();
68250
+ if (isSessionRestarting(session)) {
68251
+ transitionTo(session, "active");
68252
+ }
67984
68253
  }
67985
68254
  await options.updateSessionHeader(session);
67986
68255
  await post(session, "success", `${fmt.formatBold("Joined existing worktree")} for branch ${fmt.formatCode(branch)}
@@ -68049,7 +68318,7 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
68049
68318
  if (session.claude.isRunning()) {
68050
68319
  options.stopTyping(session);
68051
68320
  transitionTo(session, "restarting");
68052
- session.claude.kill();
68321
+ await session.claude.kill();
68053
68322
  await options.flush(session);
68054
68323
  const newSessionId = randomUUID2();
68055
68324
  session.claudeSessionId = newSessionId;
@@ -68069,10 +68338,15 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
68069
68338
  resume: false,
68070
68339
  appendSystemPrompt: await buildAppendSystemPrompt(session.platform, session.platformId, worktreePath, session.threadId, session.startedBy, session.sessionAllowedUsers, options.appendSystemPrompt ?? "", options.githubEmailsStore, { omitSessionContext: !needsTitlePrompt, userAttribution: session.userAttribution })
68071
68340
  };
68072
- session.claude = new ClaudeCli(cliOptions);
68073
- session.claude.on("event", (e) => options.handleEvent(session.sessionId, e));
68074
- session.claude.on("exit", (code) => options.handleExit(session.sessionId, code));
68075
- session.claude.start();
68341
+ session.messageManager?.clearClaudeSessionState();
68342
+ const newClaude = new ClaudeCli(cliOptions);
68343
+ session.claude = newClaude;
68344
+ newClaude.on("event", (e) => options.handleEvent(session.sessionId, e));
68345
+ newClaude.on("exit", (code) => options.handleExit(session.sessionId, code, newClaude));
68346
+ newClaude.start();
68347
+ if (isSessionRestarting(session)) {
68348
+ transitionTo(session, "active");
68349
+ }
68076
68350
  }
68077
68351
  await options.updateSessionHeader(session);
68078
68352
  const shortWorktreePath = shortenPath(worktreePath, undefined, { path: worktreePath, branch });
@@ -68342,6 +68616,9 @@ function extractAndUpdatePullRequest(text, session, ctx) {
68342
68616
  ctx.ops.updateSessionHeader(session).catch(() => {});
68343
68617
  }
68344
68618
  }
68619
+ function isSidechainEvent(event) {
68620
+ return Boolean(event.parent_tool_use_id);
68621
+ }
68345
68622
  function handleEventPreProcessing(session, event, ctx) {
68346
68623
  session.threadLogger?.logEvent(event);
68347
68624
  resetSessionActivity(session);
@@ -68363,6 +68640,16 @@ function handleEventPreProcessing(session, event, ctx) {
68363
68640
  handleCompactionComplete(session, e.compact_metadata, ctx);
68364
68641
  }
68365
68642
  }
68643
+ if (event.type === "assistant" && !isSidechainEvent(event)) {
68644
+ const msg = event.message;
68645
+ if (Array.isArray(msg?.content)) {
68646
+ for (const block of msg.content) {
68647
+ if (block.type === "tool_use" && block.name) {
68648
+ trackEvent(session, "tool_use", block.name);
68649
+ }
68650
+ }
68651
+ }
68652
+ }
68366
68653
  if (event.type === "tool_use") {
68367
68654
  const tool = event.tool_use;
68368
68655
  trackEvent(session, "tool_use", tool.name);
@@ -68374,7 +68661,9 @@ function handleEventPostProcessing(session, event, ctx) {
68374
68661
  for (const block of msg?.content || []) {
68375
68662
  if (block.type === "text" && block.text) {
68376
68663
  extractAndUpdatePullRequest(block.text, session, ctx);
68377
- detectAndExecuteClaudeCommands(block.text, session, ctx);
68664
+ if (!isSidechainEvent(event)) {
68665
+ detectAndExecuteClaudeCommands(block.text, session, ctx);
68666
+ }
68378
68667
  }
68379
68668
  }
68380
68669
  }
@@ -68384,6 +68673,16 @@ function handleEventPostProcessing(session, event, ctx) {
68384
68673
  ctx.ops.emitSessionUpdate(session.sessionId, { status: getSessionStatus(session) });
68385
68674
  updateUsageStats(session, event, ctx);
68386
68675
  }
68676
+ if (event.type === "user" && !isSidechainEvent(event)) {
68677
+ const msg = event.message;
68678
+ if (Array.isArray(msg?.content)) {
68679
+ for (const block of msg.content) {
68680
+ if (block.type === "tool_result" && block.is_error) {
68681
+ trackEvent(session, "tool_error", "Tool execution failed");
68682
+ }
68683
+ }
68684
+ }
68685
+ }
68387
68686
  if (event.type === "tool_result") {
68388
68687
  const result = event.tool_result;
68389
68688
  if (result.is_error) {
@@ -68943,17 +69242,26 @@ function commonRestartCliOptions(session, ctx) {
68943
69242
  async function restartClaudeSession(session, cliOptions, ctx, actionName) {
68944
69243
  ctx.ops.stopTyping(session);
68945
69244
  transitionTo(session, "restarting");
68946
- session.claude.kill();
69245
+ await session.claude.kill();
68947
69246
  await ctx.ops.flush(session);
68948
- session.claude = new ClaudeCli(cliOptions);
68949
- session.claude.on("event", (e) => ctx.ops.handleEvent(session.sessionId, e));
68950
- session.claude.on("exit", (code) => ctx.ops.handleExit(session.sessionId, code));
68951
- session.claude.on("rate-limit", (hit) => handleRateLimit(session, hit, ctx));
69247
+ if (!cliOptions.resume) {
69248
+ session.messageManager?.clearClaudeSessionState();
69249
+ }
69250
+ const newClaude = new ClaudeCli(cliOptions);
69251
+ session.claude = newClaude;
69252
+ newClaude.on("event", (e) => ctx.ops.handleEvent(session.sessionId, e));
69253
+ newClaude.on("exit", (code) => ctx.ops.handleExit(session.sessionId, code, newClaude));
69254
+ newClaude.on("rate-limit", (hit) => handleRateLimit(session, hit, ctx));
68952
69255
  try {
68953
- session.claude.start();
69256
+ newClaude.start();
69257
+ if (isSessionRestarting(session)) {
69258
+ transitionTo(session, "active");
69259
+ }
68954
69260
  return true;
68955
69261
  } catch (err) {
68956
- transitionTo(session, "active");
69262
+ if (isSessionRestarting(session)) {
69263
+ transitionTo(session, "active");
69264
+ }
68957
69265
  await logAndNotify(err, { action: actionName, session });
68958
69266
  return false;
68959
69267
  }
@@ -70066,7 +70374,7 @@ async function startSession(options, username, displayName, replyToPostId, platf
70066
70374
  await ctx.ops.updateStickyMessage();
70067
70375
  ctx.ops.startTyping(session);
70068
70376
  claude.on("event", (e) => ctx.ops.handleEvent(sessionId, e));
70069
- claude.on("exit", (code) => ctx.ops.handleExit(sessionId, code));
70377
+ claude.on("exit", (code) => ctx.ops.handleExit(sessionId, code, claude));
70070
70378
  claude.on("rate-limit", (hit) => handleRateLimit(session, hit, ctx));
70071
70379
  try {
70072
70380
  claude.start();
@@ -70261,7 +70569,7 @@ Please start a new session.`), { action: "Post resume failure notification" });
70261
70569
  ctx.ops.emitSessionAdd(session);
70262
70570
  keepAlive.sessionStarted();
70263
70571
  claude.on("event", (e) => ctx.ops.handleEvent(sessionId, e));
70264
- claude.on("exit", (code) => ctx.ops.handleExit(sessionId, code));
70572
+ claude.on("exit", (code) => ctx.ops.handleExit(sessionId, code, claude));
70265
70573
  claude.on("rate-limit", (hit) => handleRateLimit(session, hit, ctx));
70266
70574
  try {
70267
70575
  claude.start();
@@ -70349,7 +70657,7 @@ async function resumePausedSession(threadId, message, files, ctx, username) {
70349
70657
  log31.warn(`Failed to resume session ${shortId}..., could not send message`);
70350
70658
  }
70351
70659
  }
70352
- async function handleExit(sessionId, code, ctx) {
70660
+ async function handleExit(sessionId, code, ctx, source) {
70353
70661
  const session = mutableSessions(ctx).get(sessionId);
70354
70662
  const shortId = sessionId.substring(0, 8);
70355
70663
  sessionLog6(session).debug(`handleExit called code=${code} isShuttingDown=${ctx.state.isShuttingDown}`);
@@ -70357,6 +70665,10 @@ async function handleExit(sessionId, code, ctx) {
70357
70665
  log31.debug(`Session ${shortId}... not found (already cleaned up)`);
70358
70666
  return;
70359
70667
  }
70668
+ if (source && session.claude !== source) {
70669
+ sessionLog6(session).debug(`Ignoring exit from replaced Claude process`);
70670
+ return;
70671
+ }
70360
70672
  if (isSessionRestarting(session)) {
70361
70673
  sessionLog6(session).debug(`Restarting, skipping cleanup`);
70362
70674
  transitionTo(session, "active");
@@ -71080,7 +71392,7 @@ class SessionManager extends EventEmitter4 {
71080
71392
  updateSessionHeader: (s) => this.updateSessionHeader(s),
71081
71393
  updateStickyMessage: () => this.updateStickyMessage(),
71082
71394
  handleEvent: (sid, e) => this.handleEvent(sid, e),
71083
- handleExit: (sid, code) => this.handleExit(sid, code),
71395
+ handleExit: (sid, code, source) => this.handleExit(sid, code, source),
71084
71396
  killSession: (tid) => this.killSession(tid),
71085
71397
  shouldPromptForWorktree: (s) => this.shouldPromptForWorktree(s),
71086
71398
  postWorktreePrompt: (s, r) => this.postWorktreePrompt(s, r),
@@ -71184,8 +71496,8 @@ class SessionManager extends EventEmitter4 {
71184
71496
  session.messageManager.handleEvent(event);
71185
71497
  handleEventPostProcessing(session, event, this.getContext());
71186
71498
  }
71187
- async handleExit(sessionId, code) {
71188
- await handleExit(sessionId, code, this.getContext());
71499
+ async handleExit(sessionId, code, source) {
71500
+ await handleExit(sessionId, code, this.getContext(), source);
71189
71501
  }
71190
71502
  startTyping(session) {
71191
71503
  const wasTyping = session.timers.typingTimer !== null;
@@ -71700,7 +72012,7 @@ class SessionManager extends EventEmitter4 {
71700
72012
  worktreeMode: this.worktreeMode,
71701
72013
  permissionTimeoutMs: this.limits.permissionTimeoutSeconds * 1000,
71702
72014
  handleEvent: (tid, e) => this.handleEvent(tid, e),
71703
- handleExit: (tid, code) => this.handleExit(tid, code),
72015
+ handleExit: (tid, code, source) => this.handleExit(tid, code, source),
71704
72016
  updateSessionHeader: (s) => this.updateSessionHeader(s),
71705
72017
  flush: async (s) => {
71706
72018
  if (s.messageManager) {
@@ -48574,11 +48574,26 @@ var bashToolFormatter = {
48574
48574
  };
48575
48575
  // src/operations/tool-formatters/task-tools.ts
48576
48576
  var taskToolsFormatter = {
48577
- toolNames: ["TodoWrite", "Task", "EnterPlanMode", "ExitPlanMode", "AskUserQuestion"],
48578
- format(toolName, _input, options) {
48577
+ toolNames: [
48578
+ "TodoWrite",
48579
+ "TaskCreate",
48580
+ "TaskUpdate",
48581
+ "TaskGet",
48582
+ "TaskList",
48583
+ "Task",
48584
+ "EnterPlanMode",
48585
+ "ExitPlanMode",
48586
+ "AskUserQuestion"
48587
+ ],
48588
+ format(toolName, input, options) {
48579
48589
  const { formatter } = options;
48580
48590
  switch (toolName) {
48581
48591
  case "TodoWrite":
48592
+ case "TaskCreate":
48593
+ case "TaskUpdate":
48594
+ return { display: null, hidden: true };
48595
+ case "TaskGet":
48596
+ case "TaskList":
48582
48597
  return { display: null, hidden: true };
48583
48598
  case "Task":
48584
48599
  return { display: null, hidden: true };
@@ -48587,8 +48602,25 @@ var taskToolsFormatter = {
48587
48602
  display: `\uD83D\uDCCB ${formatter.formatBold("Planning...")}`,
48588
48603
  permissionText: `\uD83D\uDCCB ${formatter.formatBold("Planning...")}`
48589
48604
  };
48590
- case "ExitPlanMode":
48591
- return { display: null, hidden: true };
48605
+ case "ExitPlanMode": {
48606
+ const plan = typeof input.plan === "string" ? input.plan : "";
48607
+ const points = Array.from(plan);
48608
+ const truncated = points.length > 1500;
48609
+ let preview = truncated ? `${points.slice(0, 1500).join("")}
48610
+ …` : plan;
48611
+ if (truncated) {
48612
+ const fenceCount = (preview.match(/^```/gm) || []).length;
48613
+ if (fenceCount % 2 === 1)
48614
+ preview += "\n```";
48615
+ }
48616
+ return {
48617
+ display: null,
48618
+ hidden: true,
48619
+ permissionText: preview ? `\uD83D\uDCCB ${formatter.formatBold("Plan approval requested")}
48620
+
48621
+ ${preview}` : `\uD83D\uDCCB ${formatter.formatBold("Plan approval requested")}`
48622
+ };
48623
+ }
48592
48624
  case "AskUserQuestion":
48593
48625
  return { display: null, hidden: true };
48594
48626
  default:
@@ -49135,9 +49167,14 @@ function createStatusUpdateOp(sessionId, options) {
49135
49167
  }
49136
49168
  // src/operations/transformer.ts
49137
49169
  function transformEvent(event, ctx) {
49170
+ if (event.parent_tool_use_id) {
49171
+ return [];
49172
+ }
49138
49173
  switch (event.type) {
49139
49174
  case "assistant":
49140
49175
  return transformAssistant(event, ctx);
49176
+ case "user":
49177
+ return transformUser(event, ctx);
49141
49178
  case "tool_use":
49142
49179
  return transformToolUse(event, ctx);
49143
49180
  case "tool_result":
@@ -49179,6 +49216,9 @@ function transformAssistant(event, ctx) {
49179
49216
  if (result.display && !result.hidden) {
49180
49217
  flushTextBuffer();
49181
49218
  operations.push(createAppendContentOp(ctx.sessionId, result.display, true));
49219
+ if (block.id) {
49220
+ ctx.toolStartTimes.set(block.id, Date.now());
49221
+ }
49182
49222
  }
49183
49223
  }
49184
49224
  } else if (block.type === "thinking" && block.thinking) {
@@ -49193,8 +49233,70 @@ function transformAssistant(event, ctx) {
49193
49233
  }
49194
49234
  }
49195
49235
  flushTextBuffer();
49236
+ const taskListOps = operations.filter((op) => op.type === "task_list");
49237
+ if (taskListOps.length > 1) {
49238
+ const last = taskListOps[taskListOps.length - 1];
49239
+ return operations.filter((op) => op.type !== "task_list" || op === last);
49240
+ }
49196
49241
  return operations;
49197
49242
  }
49243
+ function transformUser(event, ctx) {
49244
+ const msg = event.message;
49245
+ if (!Array.isArray(msg?.content)) {
49246
+ return [];
49247
+ }
49248
+ const operations = [];
49249
+ let indicatorCount = 0;
49250
+ for (const block of msg.content) {
49251
+ if (block?.type !== "tool_result" || !block.tool_use_id)
49252
+ continue;
49253
+ if (ctx.taskTracker.hasPendingCreate(block.tool_use_id)) {
49254
+ const resolution = ctx.taskTracker.resolveCreatedId(block.tool_use_id, toolResultContentText(block.content), block.is_error === true);
49255
+ if (resolution === "removed" || resolution === "merged") {
49256
+ const action = ctx.taskTracker.allCompleted ? "complete" : "update";
49257
+ operations.push(createTaskListOp(ctx.sessionId, action, ctx.taskTracker.toTaskItems()));
49258
+ }
49259
+ }
49260
+ if (ctx.toolStartTimes.has(block.tool_use_id)) {
49261
+ operations.push(createResultIndicatorOp(block.tool_use_id, block.is_error === true, ctx));
49262
+ indicatorCount++;
49263
+ }
49264
+ }
49265
+ const taskListOps = operations.filter((op) => op.type === "task_list");
49266
+ if (taskListOps.length > 1) {
49267
+ const last = taskListOps[taskListOps.length - 1];
49268
+ const coalesced = operations.filter((op) => op.type !== "task_list" || op === last);
49269
+ operations.length = 0;
49270
+ operations.push(...coalesced);
49271
+ }
49272
+ if (indicatorCount > 0) {
49273
+ operations.push(createFlushOp(ctx.sessionId, "tool_complete"));
49274
+ }
49275
+ return operations;
49276
+ }
49277
+ function toolResultContentText(content) {
49278
+ if (typeof content === "string")
49279
+ return content;
49280
+ if (Array.isArray(content)) {
49281
+ return content.map((b) => b && typeof b === "object" && typeof b.text === "string" ? b.text : "").join(`
49282
+ `);
49283
+ }
49284
+ return "";
49285
+ }
49286
+ function createResultIndicatorOp(toolUseId, isError, ctx) {
49287
+ let elapsed = "";
49288
+ const startTime = ctx.toolStartTimes.get(toolUseId);
49289
+ if (startTime) {
49290
+ const secs = Math.round((Date.now() - startTime) / 1000);
49291
+ if (secs >= 3) {
49292
+ elapsed = ` (${secs}s)`;
49293
+ }
49294
+ ctx.toolStartTimes.delete(toolUseId);
49295
+ }
49296
+ const icon = isError ? "❌" : "✓";
49297
+ const errorNote = isError ? " Error" : "";
49298
+ return createAppendContentOp(ctx.sessionId, ` ↳ ${icon}${errorNote}${elapsed}`, true);
49299
+ }
49198
49300
  function transformToolUse(event, ctx) {
49199
49301
  const tool = event.tool_use;
49200
49302
  if (tool.id) {
@@ -49219,23 +49321,10 @@ function transformToolResult(event, ctx) {
49219
49321
  return [];
49220
49322
  }
49221
49323
  const result = event.tool_result;
49222
- const operations = [];
49223
- let elapsed = "";
49224
- if (result.tool_use_id) {
49225
- const startTime = ctx.toolStartTimes.get(result.tool_use_id);
49226
- if (startTime) {
49227
- const secs = Math.round((Date.now() - startTime) / 1000);
49228
- if (secs >= 3) {
49229
- elapsed = ` (${secs}s)`;
49230
- }
49231
- ctx.toolStartTimes.delete(result.tool_use_id);
49232
- }
49233
- }
49234
- const icon = result.is_error ? "❌" : "✓";
49235
- const errorNote = result.is_error ? " Error" : "";
49236
- operations.push(createAppendContentOp(ctx.sessionId, ` ↳ ${icon}${errorNote}${elapsed}`, true));
49237
- operations.push(createFlushOp(ctx.sessionId, "tool_complete"));
49238
- return operations;
49324
+ return [
49325
+ createResultIndicatorOp(result.tool_use_id || "", result.is_error === true, ctx),
49326
+ createFlushOp(ctx.sessionId, "tool_complete")
49327
+ ];
49239
49328
  }
49240
49329
  function transformResult(event, ctx) {
49241
49330
  const operations = [];
@@ -49252,6 +49341,10 @@ function handleSpecialTool(toolName, toolUseId, input, ctx) {
49252
49341
  switch (toolName) {
49253
49342
  case "TodoWrite":
49254
49343
  return handleTodoWrite(input, ctx);
49344
+ case "TaskCreate":
49345
+ return handleTaskCreate(toolUseId, input, ctx);
49346
+ case "TaskUpdate":
49347
+ return handleTaskUpdate(input, ctx);
49255
49348
  case "Task":
49256
49349
  return handleTaskStart(toolUseId, input, ctx);
49257
49350
  case "AskUserQuestion":
@@ -49269,10 +49362,22 @@ function handleTodoWrite(input, ctx) {
49269
49362
  status: t.status,
49270
49363
  activeForm: t.activeForm
49271
49364
  }));
49365
+ ctx.taskTracker.clear();
49272
49366
  const allCompleted = tasks.every((t) => t.status === "completed");
49273
49367
  const action = allCompleted ? "complete" : "update";
49274
49368
  return [createTaskListOp(ctx.sessionId, action, tasks)];
49275
49369
  }
49370
+ function handleTaskCreate(toolUseId, input, ctx) {
49371
+ ctx.taskTracker.create(toolUseId, input);
49372
+ return [createTaskListOp(ctx.sessionId, "update", ctx.taskTracker.toTaskItems())];
49373
+ }
49374
+ function handleTaskUpdate(input, ctx) {
49375
+ const changed = ctx.taskTracker.update(input);
49376
+ if (!changed)
49377
+ return [];
49378
+ const action = ctx.taskTracker.allCompleted ? "complete" : "update";
49379
+ return [createTaskListOp(ctx.sessionId, action, ctx.taskTracker.toTaskItems())];
49380
+ }
49276
49381
  function handleTaskStart(toolUseId, input, ctx) {
49277
49382
  const description = input.description || input.prompt || "Subagent";
49278
49383
  const subagentType = input.subagent_type || "general-purpose";
@@ -49306,6 +49411,102 @@ function truncateAtWord(text, maxLength) {
49306
49411
  }
49307
49412
  return truncated + "...";
49308
49413
  }
49414
+ // src/operations/task-tracker.ts
49415
+ var CREATED_RESULT_RE = /Task #(\S+) created/;
49416
+
49417
+ class TaskTracker {
49418
+ tasks = [];
49419
+ pendingCreates = new Map;
49420
+ unmatchedCreateResults = 0;
49421
+ create(toolUseId, input) {
49422
+ const task = {
49423
+ subject: typeof input.subject === "string" ? input.subject : "Task",
49424
+ activeForm: typeof input.activeForm === "string" ? input.activeForm : undefined,
49425
+ status: "pending"
49426
+ };
49427
+ this.tasks.push(task);
49428
+ if (toolUseId) {
49429
+ this.pendingCreates.set(toolUseId, task);
49430
+ }
49431
+ }
49432
+ hasPendingCreate(toolUseId) {
49433
+ return this.pendingCreates.has(toolUseId);
49434
+ }
49435
+ resolveCreatedId(toolUseId, resultContent, isError = false) {
49436
+ const task = this.pendingCreates.get(toolUseId);
49437
+ if (!task)
49438
+ return "ignored";
49439
+ this.pendingCreates.delete(toolUseId);
49440
+ const match = isError ? null : CREATED_RESULT_RE.exec(resultContent);
49441
+ if (!match) {
49442
+ if (!isError)
49443
+ this.unmatchedCreateResults++;
49444
+ this.tasks = this.tasks.filter((t) => t !== task);
49445
+ return "removed";
49446
+ }
49447
+ const placeholder = this.tasks.find((t) => t !== task && t.taskId === match[1]);
49448
+ if (placeholder) {
49449
+ task.status = placeholder.status;
49450
+ if (placeholder.subject !== `Task #${match[1]}`)
49451
+ task.subject = placeholder.subject;
49452
+ if (placeholder.activeForm && !task.activeForm)
49453
+ task.activeForm = placeholder.activeForm;
49454
+ this.tasks = this.tasks.filter((t) => t !== placeholder);
49455
+ }
49456
+ task.taskId = match[1];
49457
+ return placeholder ? "merged" : "resolved";
49458
+ }
49459
+ consumeUnmatchedCreateResultFlag() {
49460
+ if (this.unmatchedCreateResults === 0)
49461
+ return false;
49462
+ this.unmatchedCreateResults = 0;
49463
+ return true;
49464
+ }
49465
+ update(input) {
49466
+ const taskId = typeof input.taskId === "string" ? input.taskId : typeof input.taskId === "number" ? String(input.taskId) : undefined;
49467
+ if (!taskId)
49468
+ return false;
49469
+ let task = this.tasks.find((t) => t.taskId === taskId);
49470
+ const status = typeof input.status === "string" ? input.status : undefined;
49471
+ if (status === "deleted") {
49472
+ if (!task)
49473
+ return false;
49474
+ this.tasks = this.tasks.filter((t) => t !== task);
49475
+ return true;
49476
+ }
49477
+ if (!task) {
49478
+ task = { taskId, subject: `Task #${taskId}`, status: "pending", isPlaceholder: true };
49479
+ this.tasks.push(task);
49480
+ }
49481
+ if (typeof input.subject === "string")
49482
+ task.subject = input.subject;
49483
+ if (typeof input.activeForm === "string")
49484
+ task.activeForm = input.activeForm;
49485
+ if (status === "pending" || status === "in_progress" || status === "completed") {
49486
+ task.status = status;
49487
+ }
49488
+ return true;
49489
+ }
49490
+ toTaskItems() {
49491
+ return this.tasks.map((t) => ({
49492
+ content: t.subject,
49493
+ status: t.status,
49494
+ activeForm: t.activeForm ?? t.subject
49495
+ }));
49496
+ }
49497
+ get isEmpty() {
49498
+ return this.tasks.length === 0;
49499
+ }
49500
+ get allCompleted() {
49501
+ return this.tasks.length > 0 && this.tasks.every((t) => t.status === "completed") && this.tasks.some((t) => !t.isPlaceholder);
49502
+ }
49503
+ clear() {
49504
+ this.tasks = [];
49505
+ this.pendingCreates.clear();
49506
+ this.unmatchedCreateResults = 0;
49507
+ }
49508
+ }
49509
+
49309
49510
  // src/operations/executors/base.ts
49310
49511
  class BaseExecutor {
49311
49512
  state;
@@ -51367,6 +51568,7 @@ class MessageManager {
51367
51568
  startTypingCallback;
51368
51569
  emitSessionUpdateCallback;
51369
51570
  toolStartTimes = new Map;
51571
+ taskTracker = new TaskTracker;
51370
51572
  flushTimer = null;
51371
51573
  static DEFAULT_FLUSH_DELAY_MS = 500;
51372
51574
  flushDelayMs;
@@ -51437,10 +51639,14 @@ class MessageManager {
51437
51639
  sessionId: this.sessionId,
51438
51640
  formatter: this.platform.getFormatter(),
51439
51641
  toolStartTimes: this.toolStartTimes,
51642
+ taskTracker: this.taskTracker,
51440
51643
  detailed: true,
51441
51644
  worktreeInfo: this.worktreePath && this.worktreeBranch ? { path: this.worktreePath, branch: this.worktreeBranch } : undefined
51442
51645
  };
51443
51646
  const ops = transformEvent(event, transformCtx);
51647
+ if (this.taskTracker.consumeUnmatchedCreateResultFlag()) {
51648
+ logger.warn('TaskCreate result did not match the expected "Task #N created" wording — ' + "task dropped from the displayed list. If this repeats, the Claude CLI " + "likely changed its result text and the task display will be incomplete.");
51649
+ }
51444
51650
  if (ops.length === 0) {
51445
51651
  if (event.type !== "system") {
51446
51652
  logger.debug(`No operations from event: ${event.type}`);
@@ -51824,9 +52030,14 @@ class MessageManager {
51824
52030
  logger.debug("Reaction not handled by any executor");
51825
52031
  return false;
51826
52032
  }
52033
+ clearClaudeSessionState() {
52034
+ this.toolStartTimes.clear();
52035
+ this.taskTracker.clear();
52036
+ }
51827
52037
  reset() {
51828
52038
  this.cancelScheduledFlush();
51829
52039
  this.toolStartTimes.clear();
52040
+ this.taskTracker.clear();
51830
52041
  this.contentExecutor.reset();
51831
52042
  this.taskListExecutor.reset();
51832
52043
  this.questionApprovalExecutor.reset();
@@ -55629,6 +55840,25 @@ function extractResetAt(text, now) {
55629
55840
  }
55630
55841
  return;
55631
55842
  }
55843
+ function parseRateLimitEvent(event, now = Date.now()) {
55844
+ const info = event?.rate_limit_info;
55845
+ if (!info || typeof info !== "object")
55846
+ return { detected: false };
55847
+ const { status, resetsAt } = info;
55848
+ if (status !== "rejected")
55849
+ return { detected: false };
55850
+ const PAST_SKEW_TOLERANCE_MS = 2 * 60000;
55851
+ let resetAtEpochMs;
55852
+ if (typeof resetsAt === "number") {
55853
+ const ms = resetsAt * 1000;
55854
+ if (ms > now && ms - now < 8 * 86400000) {
55855
+ resetAtEpochMs = ms;
55856
+ } else if (ms <= now && now - ms < PAST_SKEW_TOLERANCE_MS) {
55857
+ resetAtEpochMs = now + 60000;
55858
+ }
55859
+ }
55860
+ return { detected: true, matched: `rate_limit_event status=${status}`, resetAtEpochMs };
55861
+ }
55632
55862
 
55633
55863
  // src/claude/cli.ts
55634
55864
  var log8 = createLogger("claude");
@@ -55765,6 +55995,7 @@ class ClaudeCli extends EventEmitter2 {
55765
55995
  stderrBuffer = "";
55766
55996
  mcpConfigTempFile = null;
55767
55997
  lastEmittedRateLimitDeadline = 0;
55998
+ lastEmittedHitHadExplicitReset = false;
55768
55999
  log;
55769
56000
  constructor(options) {
55770
56001
  super();
@@ -55818,6 +56049,7 @@ class ClaudeCli extends EventEmitter2 {
55818
56049
  totalStderrBytes -= this.stderrBuffer.length;
55819
56050
  this.stderrBuffer = "";
55820
56051
  this.lastEmittedRateLimitDeadline = 0;
56052
+ this.lastEmittedHitHadExplicitReset = false;
55821
56053
  cleanupBrowserBridgeSockets();
55822
56054
  const claudePath = getClaudePath();
55823
56055
  const args = [
@@ -55967,18 +56199,31 @@ class ClaudeCli extends EventEmitter2 {
55967
56199
  if (event.type === "result" && isErrorResultEvent(event)) {
55968
56200
  this.maybeEmitRateLimit(trimmed);
55969
56201
  }
56202
+ if (event.type === "rate_limit_event") {
56203
+ this.maybeEmitRateLimitHit(parseRateLimitEvent(event));
56204
+ }
55970
56205
  } catch {}
55971
56206
  }
55972
56207
  }
55973
56208
  maybeEmitRateLimit(text) {
55974
- const hit = detectRateLimit(text);
56209
+ this.maybeEmitRateLimitHit(detectRateLimit(text));
56210
+ }
56211
+ maybeEmitRateLimitHit(hit) {
55975
56212
  if (!hit.detected)
55976
56213
  return;
56214
+ if (!hit.resetAtEpochMs && this.lastEmittedHitHadExplicitReset && this.lastEmittedRateLimitDeadline > Date.now()) {
56215
+ return;
56216
+ }
55977
56217
  const newDeadline = cooldownDeadline(hit);
55978
56218
  const MIN_ADVANCE_MS = 60000;
55979
- if (newDeadline - this.lastEmittedRateLimitDeadline < MIN_ADVANCE_MS)
56219
+ if (newDeadline - this.lastEmittedRateLimitDeadline < MIN_ADVANCE_MS) {
56220
+ if (hit.resetAtEpochMs !== undefined) {
56221
+ this.lastEmittedHitHadExplicitReset = true;
56222
+ }
55980
56223
  return;
56224
+ }
55981
56225
  this.lastEmittedRateLimitDeadline = newDeadline;
56226
+ this.lastEmittedHitHadExplicitReset = hit.resetAtEpochMs !== undefined;
55982
56227
  this.log.warn(`Rate limit detected: ${hit.matched ?? "(no match text)"}`);
55983
56228
  this.emit("rate-limit", hit);
55984
56229
  }
@@ -56018,6 +56263,12 @@ class ClaudeCli extends EventEmitter2 {
56018
56263
  const pid = proc.pid;
56019
56264
  this.process = null;
56020
56265
  this.log.debug(`Killing Claude process (pid=${pid})`);
56266
+ if (process.env.INTEGRATION_TEST === "1") {
56267
+ const stack = new Error().stack?.split(`
56268
+ `).slice(2, 7).join(" > ").replace(/\s+at\s+/g, " < ") ?? "?";
56269
+ process.stderr.write(`[claude-cli kill pid=${pid}] | ${stack}
56270
+ `);
56271
+ }
56021
56272
  return new Promise((resolve5) => {
56022
56273
  this.log.debug("Sending first SIGINT");
56023
56274
  proc.kill("SIGINT");
@@ -56033,12 +56284,22 @@ class ClaudeCli extends EventEmitter2 {
56033
56284
  proc.kill("SIGTERM");
56034
56285
  } catch {}
56035
56286
  }, 2000);
56036
- proc.once("exit", (code) => {
56037
- this.log.debug(`Claude process exited (code=${code})`);
56287
+ const settle = (reason) => {
56288
+ this.log.debug(`Claude process gone (${reason})`);
56038
56289
  clearTimeout(secondSigint);
56039
56290
  clearTimeout(forceKillTimeout);
56291
+ clearTimeout(lastResort);
56040
56292
  resolve5();
56041
- });
56293
+ };
56294
+ const lastResort = setTimeout(() => {
56295
+ try {
56296
+ this.log.warn("Claude process did not exit after SIGTERM — sending SIGKILL");
56297
+ proc.kill("SIGKILL");
56298
+ } catch {}
56299
+ settle("kill timeout");
56300
+ }, 5000);
56301
+ proc.once("close", (code) => settle(`closed, code=${code}`));
56302
+ proc.once("error", () => settle("spawn error"));
56042
56303
  });
56043
56304
  }
56044
56305
  interrupt() {
@@ -58181,6 +58442,10 @@ async function handlePermissionWith(toolName, toolInput, cfg) {
58181
58442
  mcpLogger.debug(`Skipping standard prompt for ${toolName} (handler enforces its own gate)`);
58182
58443
  return { behavior: "allow", updatedInput: toolInput };
58183
58444
  }
58445
+ if (toolName === "AskUserQuestion") {
58446
+ mcpLogger.debug("Auto-allowing AskUserQuestion (question UI is handled by the main bot)");
58447
+ return { behavior: "allow", updatedInput: toolInput };
58448
+ }
58184
58449
  if (cfg.getAllowAll()) {
58185
58450
  mcpLogger.debug(`Auto-allowing ${toolName} (allow all active)`);
58186
58451
  return { behavior: "allow", updatedInput: toolInput };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.20.1",
3
+ "version": "1.21.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",