claude-threads 1.22.1 → 1.24.0

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,25 @@ 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.0] - 2026-08-08
9
+
10
+ ### Added
11
+ - **`!model` and `!effort` — switch model or reasoning effort mid-session.** Verified against the real CLI (2.1.226): `/model` and `/effort` work over stream-json (`/model` with no args lists the options; `/model sonnet` switches "for this session only"; `/effort low|medium|high|xhigh|max|auto`), and both are listed in the CLI's `init.slash_commands` — so the bot's dynamic slash-command passthrough forwards `!model sonnet` / `!effort high` as-is and the CLI's confirmation posts to the thread. Both are now first-class registered commands with unconditional forwarding handlers (like `!context`/`!cost`/`!compact` — they work even before the CLI's init event arrives, and forward their argument: `!model sonnet` → `/model sonnet`), and `!help` gained a line pointing out that Claude Code slash commands work with `!`.
12
+ - **The session header now shows the model the session actually runs on.** Previously the header picked the "primary model" by highest cumulative cost — after a `!model` switch the old model keeps the larger spend, so the header kept naming the old model indefinitely. The per-turn `init.model` (re-emitted every turn, per the reference captures) is now authoritative. Model display names also cover the Claude 5 family via generic id parsing (`claude-sonnet-5` → "Sonnet 5", `claude-fable-5` → "Fable 5", dated ids still render as before).
13
+
14
+ ### Changed
15
+ - **Latest verified Claude CLI: 2.1.226** (from 2.1.223). The full verification battery ran against it this cycle: the decision-bridge e2e (4/4), all 17 reference captures re-recorded on 2.1.225/2.1.226, and both integration matrices. Install hints updated. Also verified while probing: `/cd` and `/add-dir` report "isn't available in this environment" over stream-json on 2.1.226 — a native in-session directory switch isn't possible, so `!cd` keeps its restart-based implementation.
16
+
17
+ ## [1.23.0] - 2026-08-08
18
+
19
+ ### Fixed
20
+ - **A failed compaction no longer leaves a stale "🗜️ Compacting context..." post forever.** Captured against the real CLI (2.1.226, `real-cli-captures/compact-failed.jsonl`): a failed compact emits **no** `compact_boundary` — only a `status` event with `compact_result: "failed"` and a `compact_error` — so the in-progress post was never resolved. It now updates to "⚠️ Compaction failed (reason)". Long-lived bot threads auto-compact in production, so this state was reachable by simply keeping a session busy.
21
+
22
+ ### Added
23
+ - **Compaction completion shows real token counts.** The completion post now renders `pre → post` ("Context compacted (manual, 31k → 3k tokens)") using the `post_tokens` field verified in a new reference capture (`real-cli-captures/compact.jsonl`, recorded via a manual `/compact` driven through stream-json).
24
+ - **`auth_status` events are handled.** An auth error from the CLI mid-session (expired OAuth, revoked key) now posts a warning to the thread instead of vanishing; progress-only auth updates are logged. Shape taken from the Agent SDK's published types (`SDKAuthStatusMessage`, `@anthropic-ai/claude-agent-sdk` 0.3.226) — deliberately not capture-backed, since provoking a real auth failure requires a broken environment.
25
+ - **The mock's `persistent-session` scenario now carries the telemetry noise real streams have** (`thinking_tokens`, `post_turn_summary`, `active_goal`), pinning deliberately that the bot tolerates unconsumed event types — previously that tolerance was only proven incidentally. New `compaction`/`compaction-failed` scenarios and an integration suite cover the compaction lifecycle end to end on both platforms.
26
+
8
27
  ## [1.22.1] - 2026-08-08
9
28
 
10
29
  ### Fixed
package/dist/index.js CHANGED
@@ -51319,7 +51319,7 @@ var COMMON_CLAUDE_PATHS = process.platform === "win32" ? [
51319
51319
  var CLAUDE_CLI_MIN_VERSION = "2.0.74";
51320
51320
  var CLAUDE_CLI_VERIFIED_RANGE = ">=2.0.74 <2.2.0";
51321
51321
  var CLAUDE_CLI_SUPPORTED_MAJOR = 2;
51322
- var CLAUDE_CLI_LATEST_VERIFIED = "2.1.223";
51322
+ var CLAUDE_CLI_LATEST_VERIFIED = "2.1.226";
51323
51323
  function tryClaudeVersion(claudePath) {
51324
51324
  try {
51325
51325
  const output = execSync(`"${claudePath}" --version`, {
@@ -57560,6 +57560,18 @@ var COMMAND_REGISTRY = [
57560
57560
  description: "Compact conversation",
57561
57561
  category: "passthrough",
57562
57562
  audience: "both"
57563
+ },
57564
+ {
57565
+ command: "model",
57566
+ description: "Show or switch the model for this session (e.g. !model sonnet)",
57567
+ category: "passthrough",
57568
+ audience: "both"
57569
+ },
57570
+ {
57571
+ command: "effort",
57572
+ description: "Set reasoning effort: low|medium|high|xhigh|max|auto (e.g. !effort high)",
57573
+ category: "passthrough",
57574
+ audience: "both"
57563
57575
  }
57564
57576
  ];
57565
57577
  var REACTION_REGISTRY = [
@@ -57617,6 +57629,8 @@ var COMMAND_PATTERNS = [
57617
57629
  ["context", /^!context\s*$/i],
57618
57630
  ["cost", /^!cost\s*$/i],
57619
57631
  ["compact", /^!compact\s*$/i],
57632
+ ["model", /^!model(?:\s+(\S+))?\s*$/i],
57633
+ ["effort", /^!effort(?:\s+(\S+))?\s*$/i],
57620
57634
  ["plugin", /^!plugin(?:\s+(.+))?$/i],
57621
57635
  ["kill", /^!kill\s*$/i],
57622
57636
  ["bug", /^!bug(?:\s+(.+))?$/i],
@@ -57741,6 +57755,8 @@ ${formatter.formatBold("Reactions:")}
57741
57755
  ` + `${formatter.formatListItem(approvalReactions)}
57742
57756
  ` + `${formatter.formatListItem(sessionReactions)}
57743
57757
 
57758
+ ` + `Claude Code slash commands work with ${code("!")} too — e.g. ${code("!model sonnet")}, ${code("!effort high")}, ${code("!compact")}, ${code("!context")}.
57759
+
57744
57760
  ` + formatSponsorFooter(formatter);
57745
57761
  }
57746
57762
 
@@ -58122,12 +58138,13 @@ var handlePlugin = async (ctx, args) => {
58122
58138
  return { handled: true };
58123
58139
  };
58124
58140
  function createPassthroughHandler(slashCommand) {
58125
- return async (ctx) => {
58141
+ return async (ctx, args) => {
58126
58142
  if (ctx.commandContext === "first-message") {
58127
58143
  return { handled: false };
58128
58144
  }
58129
58145
  if (ctx.isAllowed) {
58130
- await ctx.sessionManager.sendFollowUp(ctx.threadId, `/${slashCommand}`, undefined, undefined, undefined, { system: true });
58146
+ const full = args ? `/${slashCommand} ${args}` : `/${slashCommand}`;
58147
+ await ctx.sessionManager.sendFollowUp(ctx.threadId, full, undefined, undefined, undefined, { system: true });
58131
58148
  }
58132
58149
  return { handled: true };
58133
58150
  };
@@ -58150,6 +58167,8 @@ handlers.set("plugin", handlePlugin);
58150
58167
  handlers.set("context", createPassthroughHandler("context"));
58151
58168
  handlers.set("cost", createPassthroughHandler("cost"));
58152
58169
  handlers.set("compact", createPassthroughHandler("compact"));
58170
+ handlers.set("model", createPassthroughHandler("model"));
58171
+ handlers.set("effort", createPassthroughHandler("effort"));
58153
58172
  async function executeCommand(command, args, ctx) {
58154
58173
  const cmdDef = getCommandDef(command);
58155
58174
  if (!cmdDef) {
@@ -68841,6 +68860,9 @@ function handleEventPreProcessing(session, event, ctx) {
68841
68860
  }
68842
68861
  if (event.type === "system") {
68843
68862
  const e = event;
68863
+ if (e.subtype === "init" && typeof e.model === "string") {
68864
+ session.currentModel = e.model;
68865
+ }
68844
68866
  if (e.subtype === "init" && e.slash_commands && Array.isArray(e.slash_commands)) {
68845
68867
  session.availableSlashCommands = new Set(e.slash_commands.map((cmd) => cmd.startsWith("/") ? cmd.slice(1) : cmd));
68846
68868
  sessionLog3(session).info(`Captured ${session.availableSlashCommands.size} slash commands from init: ${[...session.availableSlashCommands].join(", ")}`);
@@ -68848,10 +68870,25 @@ function handleEventPreProcessing(session, event, ctx) {
68848
68870
  if (e.subtype === "status" && e.status === "compacting") {
68849
68871
  handleCompactionStart(session, ctx);
68850
68872
  }
68873
+ if (e.subtype === "status" && e.compact_result === "failed") {
68874
+ handleCompactionFailed(session, e.compact_error, ctx);
68875
+ }
68851
68876
  if (e.subtype === "compact_boundary") {
68852
68877
  handleCompactionComplete(session, e.compact_metadata, ctx);
68853
68878
  }
68854
68879
  }
68880
+ if (event.type === "auth_status") {
68881
+ const e = event;
68882
+ if (e.error) {
68883
+ sessionLog3(session).warn(`\uD83D\uDD10 Claude CLI auth error: ${e.error}`);
68884
+ if (session.lastAuthErrorPosted !== e.error) {
68885
+ session.lastAuthErrorPosted = e.error;
68886
+ withErrorHandling(() => post(session, "warning", `\uD83D\uDD10 Claude CLI authentication problem: ${e.error}`), { action: "Post auth status warning", session });
68887
+ }
68888
+ } else {
68889
+ sessionLog3(session).info(`\uD83D\uDD10 Claude CLI auth status: authenticating=${e.isAuthenticating ?? false}${e.output?.length ? ` (${e.output[e.output.length - 1]})` : ""}`);
68890
+ }
68891
+ }
68855
68892
  if (event.type === "assistant" && !isSidechainEvent(event)) {
68856
68893
  const msg = event.message;
68857
68894
  if (Array.isArray(msg?.content)) {
@@ -68908,21 +68945,46 @@ function handleEventPostProcessing(session, event, ctx) {
68908
68945
  }
68909
68946
  }
68910
68947
  }
68911
- async function handleCompactionStart(session, _ctx) {
68912
- await session.messageManager?.closeCurrentPost();
68948
+ function handleCompactionStart(session, _ctx) {
68949
+ session.compactionPostPromise = (async () => {
68950
+ await session.messageManager?.closeCurrentPost();
68951
+ const formatter = session.platform.getFormatter();
68952
+ const message = `\uD83D\uDDDC️ ${formatter.formatBold("Compacting context...")} ${formatter.formatItalic("(freeing up memory)")}`;
68953
+ const compactionPost = await withErrorHandling(() => post(session, "info", message), { action: "Post compaction start", session });
68954
+ if (compactionPost) {
68955
+ session.compactionPostId = compactionPost.id;
68956
+ }
68957
+ })();
68958
+ }
68959
+ async function awaitCompactionStartPost(session) {
68960
+ if (session.compactionPostPromise) {
68961
+ await session.compactionPostPromise.catch(() => {});
68962
+ session.compactionPostPromise = undefined;
68963
+ }
68964
+ }
68965
+ async function handleCompactionFailed(session, compactError, _ctx) {
68966
+ await awaitCompactionStartPost(session);
68913
68967
  const formatter = session.platform.getFormatter();
68914
- const message = `\uD83D\uDDDC️ ${formatter.formatBold("Compacting context...")} ${formatter.formatItalic("(freeing up memory)")}`;
68915
- const compactionPost = await withErrorHandling(() => post(session, "info", message), { action: "Post compaction start", session });
68916
- if (compactionPost) {
68917
- session.compactionPostId = compactionPost.id;
68968
+ const reason = compactError || "unknown error";
68969
+ const message = `⚠️ ${formatter.formatBold("Compaction failed")} ${formatter.formatItalic(`(${reason})`)}`;
68970
+ const startPostId = session.compactionPostId;
68971
+ if (startPostId) {
68972
+ await withErrorHandling(() => updatePost(session, startPostId, message), { action: "Update compaction post (failed)", session });
68973
+ session.compactionPostId = undefined;
68974
+ } else {
68975
+ await withErrorHandling(() => post(session, "info", message), { action: "Post compaction failure", session });
68918
68976
  }
68919
68977
  }
68920
68978
  async function handleCompactionComplete(session, compactMetadata, _ctx) {
68979
+ await awaitCompactionStartPost(session);
68921
68980
  const metadata = compactMetadata;
68922
68981
  const trigger = metadata?.trigger || "auto";
68923
68982
  const preTokens = metadata?.pre_tokens;
68983
+ const postTokens = metadata?.post_tokens;
68924
68984
  let info = trigger === "manual" ? "manual" : "auto";
68925
- if (preTokens && preTokens > 0) {
68985
+ if (preTokens && preTokens > 0 && postTokens && postTokens > 0) {
68986
+ info += `, ${Math.round(preTokens / 1000)}k → ${Math.max(1, Math.round(postTokens / 1000))}k tokens`;
68987
+ } else if (preTokens && preTokens > 0) {
68926
68988
  info += `, ${Math.round(preTokens / 1000)}k tokens`;
68927
68989
  }
68928
68990
  const formatter = session.platform.getFormatter();
@@ -68935,20 +68997,15 @@ async function handleCompactionComplete(session, compactMetadata, _ctx) {
68935
68997
  }
68936
68998
  }
68937
68999
  function getModelDisplayName(modelId) {
68938
- if (modelId.includes("opus-4-5") || modelId.includes("opus-4.5"))
68939
- return "Opus 4.5";
68940
- if (modelId.includes("opus-4"))
68941
- return "Opus 4";
68942
- if (modelId.includes("opus"))
68943
- return "Opus";
68944
- if (modelId.includes("sonnet-4"))
68945
- return "Sonnet 4";
68946
- if (modelId.includes("sonnet-3-5") || modelId.includes("sonnet-3.5"))
68947
- return "Sonnet 3.5";
69000
+ const modern = modelId.match(/^claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?:-\d{8})?$/);
69001
+ if (modern) {
69002
+ const family = modern[1].charAt(0).toUpperCase() + modern[1].slice(1);
69003
+ return modern[3] ? `${family} ${modern[2]}.${modern[3]}` : `${family} ${modern[2]}`;
69004
+ }
68948
69005
  if (modelId.includes("sonnet"))
68949
69006
  return "Sonnet";
68950
- if (modelId.includes("haiku-4-5") || modelId.includes("haiku-4.5"))
68951
- return "Haiku 4.5";
69007
+ if (modelId.includes("opus"))
69008
+ return "Opus";
68952
69009
  if (modelId.includes("haiku"))
68953
69010
  return "Haiku";
68954
69011
  const match = modelId.match(/claude-(\w+)/);
@@ -68979,6 +69036,10 @@ function updateUsageStats(session, event, ctx) {
68979
69036
  contextWindowSize = usage.contextWindow;
68980
69037
  }
68981
69038
  }
69039
+ if (session.currentModel && result.modelUsage[session.currentModel]) {
69040
+ primaryModel = session.currentModel;
69041
+ contextWindowSize = result.modelUsage[session.currentModel].contextWindow;
69042
+ }
68982
69043
  let contextTokens = 0;
68983
69044
  if (result.usage) {
68984
69045
  contextTokens = result.usage.input_tokens + result.usage.cache_creation_input_tokens + result.usage.cache_read_input_tokens;
@@ -56734,6 +56734,18 @@ var COMMAND_REGISTRY = [
56734
56734
  description: "Compact conversation",
56735
56735
  category: "passthrough",
56736
56736
  audience: "both"
56737
+ },
56738
+ {
56739
+ command: "model",
56740
+ description: "Show or switch the model for this session (e.g. !model sonnet)",
56741
+ category: "passthrough",
56742
+ audience: "both"
56743
+ },
56744
+ {
56745
+ command: "effort",
56746
+ description: "Set reasoning effort: low|medium|high|xhigh|max|auto (e.g. !effort high)",
56747
+ category: "passthrough",
56748
+ audience: "both"
56737
56749
  }
56738
56750
  ];
56739
56751
  var REACTION_REGISTRY = [
@@ -56806,6 +56818,8 @@ ${formatter.formatBold("Reactions:")}
56806
56818
  ` + `${formatter.formatListItem(approvalReactions)}
56807
56819
  ` + `${formatter.formatListItem(sessionReactions)}
56808
56820
 
56821
+ ` + `Claude Code slash commands work with ${code("!")} too — e.g. ${code("!model sonnet")}, ${code("!effort high")}, ${code("!compact")}, ${code("!context")}.
56822
+
56809
56823
  ` + formatSponsorFooter(formatter);
56810
56824
  }
56811
56825
 
@@ -57072,12 +57086,13 @@ var handlePlugin = async (ctx, args) => {
57072
57086
  return { handled: true };
57073
57087
  };
57074
57088
  function createPassthroughHandler(slashCommand) {
57075
- return async (ctx) => {
57089
+ return async (ctx, args) => {
57076
57090
  if (ctx.commandContext === "first-message") {
57077
57091
  return { handled: false };
57078
57092
  }
57079
57093
  if (ctx.isAllowed) {
57080
- await ctx.sessionManager.sendFollowUp(ctx.threadId, `/${slashCommand}`, undefined, undefined, undefined, { system: true });
57094
+ const full = args ? `/${slashCommand} ${args}` : `/${slashCommand}`;
57095
+ await ctx.sessionManager.sendFollowUp(ctx.threadId, full, undefined, undefined, undefined, { system: true });
57081
57096
  }
57082
57097
  return { handled: true };
57083
57098
  };
@@ -57100,6 +57115,8 @@ handlers.set("plugin", handlePlugin);
57100
57115
  handlers.set("context", createPassthroughHandler("context"));
57101
57116
  handlers.set("cost", createPassthroughHandler("cost"));
57102
57117
  handlers.set("compact", createPassthroughHandler("compact"));
57118
+ handlers.set("model", createPassthroughHandler("model"));
57119
+ handlers.set("effort", createPassthroughHandler("effort"));
57103
57120
  // src/commands/system-prompt-generator.ts
57104
57121
  var log9 = createLogger("system-prompt");
57105
57122
  function formatUserCommand(cmd) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "1.22.1",
3
+ "version": "1.24.0",
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",