claude-threads 0.57.4 → 0.58.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
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.58.0] - 2026-01-10
11
+
12
+ ### Improved
13
+ - **More stable session titles** - Titles now stay consistent throughout the session by anchoring on the original task rather than constantly changing based on recent messages (#189)
14
+ - Original task is used as the PRIMARY anchor for title generation
15
+ - Recent context only matters if the session focus fundamentally changed
16
+ - Existing title is preserved unless there's a major direction shift
17
+
18
+ ### Removed
19
+ - **Dead code cleanup** - Removed obsolete marker-based metadata extraction from events.ts (title/description now generated out-of-band via quickQuery)
20
+
10
21
  ## [0.57.0] - 2026-01-10
11
22
 
12
23
  ### Added
package/dist/index.js CHANGED
@@ -51857,6 +51857,7 @@ function formatContextBar(percent) {
51857
51857
  async function cancelSession(session, username, ctx) {
51858
51858
  sessionLog2(session).info(`\uD83D\uDED1 Cancelled by @${username}`);
51859
51859
  session.threadLogger?.logCommand("stop", undefined, username);
51860
+ session.isCancelled = true;
51860
51861
  const formatter = session.platform.getFormatter();
51861
51862
  await postCancelled(session, `${formatter.formatBold("Session cancelled")} by ${formatter.formatUserMention(username)}`);
51862
51863
  await ctx.ops.killSession(session.threadId);
@@ -53062,52 +53063,6 @@ var log16 = createLogger("events");
53062
53063
  function sessionLog4(session) {
53063
53064
  return log16.forSession(session.sessionId);
53064
53065
  }
53065
- function extractAndUpdateMetadata(text, session, config, sessionField, ctx) {
53066
- const regex2 = new RegExp(`\\[${config.marker}:\\s*([^\\]]+)\\]`);
53067
- const match = text.match(regex2);
53068
- if (sessionField === "sessionTitle") {
53069
- const textPreview = text.substring(0, 200).replace(/\n/g, "\\n");
53070
- log16.forSession(session.sessionId).debug(`Title extraction: match=${match ? `"${match[1]}"` : "null"}, text="${textPreview}${text.length > 200 ? "..." : ""}"`);
53071
- }
53072
- if (match) {
53073
- const newValue = match[1].trim();
53074
- const isValid = newValue.length >= config.minLength && newValue.length <= config.maxLength && !/^\.+$/.test(newValue) && !/^\u2026+$/.test(newValue) && newValue !== config.placeholder && !newValue.startsWith("...");
53075
- if (sessionField === "sessionTitle") {
53076
- log16.forSession(session.sessionId).debug(`Title validation: value="${newValue}", len=${newValue.length}, isValid=${isValid}, current="${session[sessionField]}"`);
53077
- }
53078
- if (isValid && newValue !== session[sessionField]) {
53079
- session[sessionField] = newValue;
53080
- log16.forSession(session.sessionId).debug(`Setting ${sessionField} to "${newValue}"`);
53081
- ctx.ops.persistSession(session);
53082
- ctx.ops.updateStickyMessage().catch((err) => {
53083
- log16.forSession(session.sessionId).error(`Failed to update sticky message: ${err}`);
53084
- });
53085
- ctx.ops.updateSessionHeader(session).catch((err) => {
53086
- log16.forSession(session.sessionId).error(`Failed to update session header: ${err}`);
53087
- });
53088
- const updates = {};
53089
- if (sessionField === "sessionTitle")
53090
- updates.title = newValue;
53091
- if (sessionField === "sessionDescription")
53092
- updates.description = newValue;
53093
- ctx.ops.emitSessionUpdate(session.sessionId, updates);
53094
- }
53095
- }
53096
- const removeRegex = new RegExp(`\\[${config.marker}:\\s*[^\\]]+\\]\\s*`, "g");
53097
- return text.replace(removeRegex, "").trim();
53098
- }
53099
- var TITLE_CONFIG = {
53100
- marker: "SESSION_TITLE",
53101
- minLength: 3,
53102
- maxLength: 50,
53103
- placeholder: "<short title>"
53104
- };
53105
- var DESCRIPTION_CONFIG = {
53106
- marker: "SESSION_DESCRIPTION",
53107
- minLength: 5,
53108
- maxLength: 100,
53109
- placeholder: "<brief description>"
53110
- };
53111
53066
  function detectAndExecuteClaudeCommands(text, session, ctx) {
53112
53067
  const parsed = parseClaudeCommand(text);
53113
53068
  if (parsed && isClaudeAllowedCommand(parsed.command)) {
@@ -53235,8 +53190,6 @@ function formatEvent(session, e, ctx) {
53235
53190
  for (const block of msg?.content || []) {
53236
53191
  if (block.type === "text" && block.text) {
53237
53192
  let text = block.text.replace(/<thinking>[\s\S]*?<\/thinking>/g, "").trim();
53238
- text = extractAndUpdateMetadata(text, session, TITLE_CONFIG, "sessionTitle", ctx);
53239
- text = extractAndUpdateMetadata(text, session, DESCRIPTION_CONFIG, "sessionDescription", ctx);
53240
53193
  extractAndUpdatePullRequest(text, session, ctx);
53241
53194
  text = detectAndExecuteClaudeCommands(text, session, ctx);
53242
53195
  if (text)
@@ -53993,11 +53946,14 @@ var MIN_TITLE_LENGTH = 3;
53993
53946
  var MAX_TITLE_LENGTH = 50;
53994
53947
  var MIN_DESC_LENGTH = 5;
53995
53948
  var MAX_DESC_LENGTH = 200;
53996
- function buildTitlePrompt(userMessage) {
53997
- const truncatedMessage = userMessage.length > 500 ? userMessage.substring(0, 500) + "..." : userMessage;
53998
- return `Generate a session title and description for this task.
53949
+ var MAX_ORIGINAL_TASK_LENGTH = 1000;
53950
+ var MAX_RECENT_CONTEXT_LENGTH = 500;
53951
+ function buildTitlePrompt(context) {
53952
+ if (typeof context === "string") {
53953
+ const truncated = context.length > MAX_ORIGINAL_TASK_LENGTH ? context.substring(0, MAX_ORIGINAL_TASK_LENGTH) + "..." : context;
53954
+ return `Generate a session title and description for this task.
53999
53955
 
54000
- Task: "${truncatedMessage}"
53956
+ Task: "${truncated}"
54001
53957
 
54002
53958
  Rules for title:
54003
53959
  - 3-7 words, imperative form (e.g., "Fix login bug", "Add dark mode")
@@ -54008,6 +53964,38 @@ Rules for description:
54008
53964
  - 1-2 sentences, under 100 characters total
54009
53965
  - Explain what will be accomplished
54010
53966
 
53967
+ Output format (exactly two lines):
53968
+ TITLE: <title here>
53969
+ DESC: <description here>`;
53970
+ }
53971
+ const { originalTask, recentContext, currentTitle } = context;
53972
+ const truncatedOriginal = originalTask.length > MAX_ORIGINAL_TASK_LENGTH ? originalTask.substring(0, MAX_ORIGINAL_TASK_LENGTH) + "..." : originalTask;
53973
+ const truncatedRecent = recentContext && recentContext.length > MAX_RECENT_CONTEXT_LENGTH ? recentContext.substring(0, MAX_RECENT_CONTEXT_LENGTH) + "..." : recentContext;
53974
+ let contextSection = `Original task (PRIMARY - base the title on this): "${truncatedOriginal}"`;
53975
+ if (truncatedRecent) {
53976
+ contextSection += `
53977
+
53978
+ Recent activity (SECONDARY - only incorporate if the session focus has fundamentally shifted): "${truncatedRecent}"`;
53979
+ }
53980
+ const stabilityInstruction = currentTitle ? `
53981
+
53982
+ Current title: "${currentTitle}"
53983
+ IMPORTANT: Only suggest a different title if the session focus has FUNDAMENTALLY changed. Minor variations in activity should NOT change the title. Prefer keeping the current title if it still captures the main goal.` : "";
53984
+ return `Generate a session title and description based on the following context.
53985
+ ${stabilityInstruction}
53986
+
53987
+ ${contextSection}
53988
+
53989
+ Rules for title:
53990
+ - 3-7 words, imperative form (e.g., "Fix login bug", "Add dark mode")
53991
+ - No quotes or punctuation at end
53992
+ - Capture the MAIN intent from the original task
53993
+ - Only deviate from original task if recent activity shows a fundamental change in direction
53994
+
53995
+ Rules for description:
53996
+ - 1-2 sentences, under 100 characters total
53997
+ - Explain what will be accomplished
53998
+
54011
53999
  Output format (exactly two lines):
54012
54000
  TITLE: <title here>
54013
54001
  DESC: <description here>`;
@@ -54039,11 +54027,12 @@ function parseMetadata(response) {
54039
54027
  }
54040
54028
  return { title, description };
54041
54029
  }
54042
- async function suggestSessionMetadata(userMessage) {
54043
- log18.debug(`Suggesting title for: "${userMessage.substring(0, 50)}..."`);
54030
+ async function suggestSessionMetadata(context) {
54031
+ const logContext = typeof context === "string" ? context.substring(0, 50) : context.originalTask.substring(0, 50);
54032
+ log18.debug(`Suggesting title for: "${logContext}..."`);
54044
54033
  try {
54045
54034
  const result = await quickQuery({
54046
- prompt: buildTitlePrompt(userMessage),
54035
+ prompt: buildTitlePrompt(context),
54047
54036
  model: "haiku",
54048
54037
  timeout: SUGGESTION_TIMEOUT2
54049
54038
  });
@@ -54206,12 +54195,17 @@ function firePeriodicReclassification(session, currentMessage, ctx) {
54206
54195
  (async () => {
54207
54196
  try {
54208
54197
  const sessionId = session.sessionId;
54209
- const contextPrompt = session.firstPrompt ? `Original task: ${session.firstPrompt}
54210
-
54211
- Current message: ${currentMessage}` : currentMessage;
54198
+ const titleContext = session.firstPrompt ? {
54199
+ originalTask: session.firstPrompt,
54200
+ recentContext: currentMessage,
54201
+ currentTitle: session.sessionTitle
54202
+ } : currentMessage;
54203
+ const tagContext = session.firstPrompt ? `Original task: ${session.firstPrompt}
54204
+
54205
+ Recent activity: ${currentMessage}` : currentMessage;
54212
54206
  const [metadata, tags] = await Promise.all([
54213
- suggestSessionMetadata(contextPrompt),
54214
- suggestSessionTags(contextPrompt)
54207
+ suggestSessionMetadata(titleContext),
54208
+ suggestSessionTags(tagContext)
54215
54209
  ]);
54216
54210
  const currentSession = ctx.state.sessions.get(sessionId);
54217
54211
  if (!currentSession) {
@@ -54220,10 +54214,14 @@ Current message: ${currentMessage}` : currentMessage;
54220
54214
  }
54221
54215
  let updated = false;
54222
54216
  if (metadata) {
54223
- currentSession.sessionTitle = metadata.title;
54224
- currentSession.sessionDescription = metadata.description;
54225
- sessionLog6(currentSession).debug(`Updated title: "${metadata.title}"`);
54226
- updated = true;
54217
+ if (metadata.title !== currentSession.sessionTitle) {
54218
+ currentSession.sessionTitle = metadata.title;
54219
+ currentSession.sessionDescription = metadata.description;
54220
+ sessionLog6(currentSession).debug(`Updated title: "${metadata.title}"`);
54221
+ updated = true;
54222
+ } else {
54223
+ sessionLog6(currentSession).debug("Title unchanged (stable)");
54224
+ }
54227
54225
  }
54228
54226
  if (tags.length > 0) {
54229
54227
  currentSession.sessionTags = tags;
@@ -54372,6 +54370,7 @@ ${CHAT_PLATFORM_PROMPT}`;
54372
54370
  subagentUpdateTimer: null,
54373
54371
  timeoutWarningPosted: false,
54374
54372
  isRestarting: false,
54373
+ isCancelled: false,
54375
54374
  isResumed: false,
54376
54375
  resumeFailCount: 0,
54377
54376
  wasInterrupted: false,
@@ -54524,6 +54523,7 @@ ${CHAT_PLATFORM_PROMPT}`;
54524
54523
  subagentUpdateTimer: null,
54525
54524
  timeoutWarningPosted: false,
54526
54525
  isRestarting: false,
54526
+ isCancelled: false,
54527
54527
  isResumed: true,
54528
54528
  resumeFailCount: state.resumeFailCount ?? 0,
54529
54529
  wasInterrupted: false,
@@ -54660,6 +54660,10 @@ async function handleExit(sessionId, code, ctx) {
54660
54660
  session.isRestarting = false;
54661
54661
  return;
54662
54662
  }
54663
+ if (session.isCancelled) {
54664
+ sessionLog6(session).debug(`Cancelled, skipping cleanup (handled by killSession)`);
54665
+ return;
54666
+ }
54663
54667
  if (ctx.state.isShuttingDown) {
54664
54668
  sessionLog6(session).debug(`Bot shutting down, preserving persistence`);
54665
54669
  ctx.ops.stopTyping(session);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "0.57.4",
3
+ "version": "0.58.0",
4
4
  "description": "Share Claude Code sessions live in a Mattermost channel with interactive features",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",