mattermost-claude-code 0.9.2 → 0.9.3

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,18 @@ 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
+ ## [0.9.3] - 2025-12-28
9
+
10
+ ### Fixed
11
+ - **Major fix for session persistence** - completely rewrote session lifecycle management
12
+ - Sessions now correctly survive bot restarts (was broken in 0.9.0-0.9.2)
13
+ - `killAllSessions()` now explicitly preserves persistence instead of relying on exit event timing
14
+ - `killSession()` now takes an `unpersist` parameter to control persistence behavior
15
+ - `handleExit()` now only unpersists on graceful exits (code 0), not on errors
16
+ - Resumed sessions that fail are preserved for retry instead of being removed
17
+ - Added comprehensive debug logging to trace session lifecycle
18
+ - Race condition between shutdown and exit events eliminated
19
+
8
20
  ## [0.9.2] - 2025-12-28
9
21
 
10
22
  ### Fixed
@@ -94,6 +94,8 @@ export declare class SessionManager {
94
94
  getSessionCount(): number;
95
95
  /** Get all active session thread IDs */
96
96
  getActiveThreadIds(): string[];
97
+ /** Mark that we're shutting down (prevents cleanup of persisted sessions) */
98
+ setShuttingDown(): void;
97
99
  /** Register a post for reaction routing */
98
100
  private registerPost;
99
101
  /** Find session by post ID (for reaction routing) */
@@ -138,7 +140,7 @@ export declare class SessionManager {
138
140
  /** Send a follow-up message to an existing session */
139
141
  sendFollowUp(threadId: string, message: string, files?: MattermostFile[]): Promise<void>;
140
142
  /** Kill a specific session */
141
- killSession(threadId: string): void;
143
+ killSession(threadId: string, unpersist?: boolean): void;
142
144
  /** Cancel a session with user feedback */
143
145
  cancelSession(threadId: string, username: string): Promise<void>;
144
146
  /** Change working directory for a session (restarts Claude CLI) */
@@ -180,6 +180,8 @@ export class SessionManager {
180
180
  * Persist a session to disk
181
181
  */
182
182
  persistSession(session) {
183
+ const shortId = session.threadId.substring(0, 8);
184
+ console.log(` [persist] Saving session ${shortId}...`);
183
185
  const state = {
184
186
  threadId: session.threadId,
185
187
  claudeSessionId: session.claudeSessionId,
@@ -195,19 +197,14 @@ export class SessionManager {
195
197
  planApproved: session.planApproved,
196
198
  };
197
199
  this.sessionStore.save(session.threadId, state);
198
- if (this.debug) {
199
- const shortId = session.threadId.substring(0, 8);
200
- console.log(` [persist] Saved session ${shortId}... (claudeId: ${session.claudeSessionId.substring(0, 8)}...)`);
201
- }
200
+ console.log(` [persist] Saved session ${shortId}... (claudeId: ${session.claudeSessionId.substring(0, 8)}...)`);
202
201
  }
203
202
  /**
204
203
  * Remove a session from persistence
205
204
  */
206
205
  unpersistSession(threadId) {
207
- if (this.debug) {
208
- const shortId = threadId.substring(0, 8);
209
- console.log(` [persist] Removing session ${shortId}...`);
210
- }
206
+ const shortId = threadId.substring(0, 8);
207
+ console.log(` [persist] REMOVING session ${shortId}... (this should NOT happen during shutdown!)`);
211
208
  this.sessionStore.remove(threadId);
212
209
  }
213
210
  // ---------------------------------------------------------------------------
@@ -229,6 +226,11 @@ export class SessionManager {
229
226
  getActiveThreadIds() {
230
227
  return [...this.sessions.keys()];
231
228
  }
229
+ /** Mark that we're shutting down (prevents cleanup of persisted sessions) */
230
+ setShuttingDown() {
231
+ console.log(' [shutdown] Setting isShuttingDown = true');
232
+ this.isShuttingDown = true;
233
+ }
232
234
  /** Register a post for reaction routing */
233
235
  registerPost(postId, threadId) {
234
236
  this.postIndex.set(postId, threadId);
@@ -947,26 +949,52 @@ export class SessionManager {
947
949
  async handleExit(threadId, code) {
948
950
  const session = this.sessions.get(threadId);
949
951
  const shortId = threadId.substring(0, 8);
952
+ // Always log exit events to trace the flow
953
+ console.log(` [exit] handleExit called for ${shortId}... code=${code} isShuttingDown=${this.isShuttingDown}`);
950
954
  if (!session) {
951
- if (this.debug)
952
- console.log(` [exit] Session ${shortId}... not found (already cleaned up)`);
955
+ console.log(` [exit] Session ${shortId}... not found (already cleaned up)`);
953
956
  return;
954
957
  }
955
958
  // If we're intentionally restarting (e.g., !cd), don't clean up or post exit message
956
959
  if (session.isRestarting) {
957
- if (this.debug)
958
- console.log(` [exit] Session ${shortId}... restarting, skipping cleanup`);
960
+ console.log(` [exit] Session ${shortId}... restarting, skipping cleanup`);
959
961
  session.isRestarting = false; // Reset flag here, after the exit event fires
960
962
  return;
961
963
  }
962
964
  // If bot is shutting down, suppress exit messages (shutdown message already sent)
965
+ // IMPORTANT: Check this flag FIRST before any cleanup. The session should remain
966
+ // persisted so it can be resumed after restart.
963
967
  if (this.isShuttingDown) {
964
- if (this.debug)
965
- console.log(` [exit] Session ${shortId}... bot shutting down, preserving persistence`);
968
+ console.log(` [exit] Session ${shortId}... bot shutting down, preserving persistence`);
969
+ // Still clean up from in-memory maps since we're shutting down anyway
970
+ this.stopTyping(session);
971
+ if (session.updateTimer) {
972
+ clearTimeout(session.updateTimer);
973
+ session.updateTimer = null;
974
+ }
975
+ this.sessions.delete(threadId);
966
976
  return;
967
977
  }
968
- if (this.debug)
969
- console.log(` [exit] Session ${shortId}... exited with code ${code}, cleaning up`);
978
+ // For resumed sessions that exit quickly (e.g., Claude --resume fails),
979
+ // don't unpersist immediately - give it a chance to be retried
980
+ if (session.isResumed && code !== 0) {
981
+ console.log(` [exit] Resumed session ${shortId}... failed with code ${code}, preserving for retry`);
982
+ this.stopTyping(session);
983
+ if (session.updateTimer) {
984
+ clearTimeout(session.updateTimer);
985
+ session.updateTimer = null;
986
+ }
987
+ this.sessions.delete(threadId);
988
+ // Post error message but keep persistence
989
+ try {
990
+ await this.mattermost.createPost(`⚠️ **Session resume failed** (exit code ${code}). The session data is preserved - try restarting the bot.`, session.threadId);
991
+ }
992
+ catch {
993
+ // Ignore if we can't post
994
+ }
995
+ return;
996
+ }
997
+ console.log(` [exit] Session ${shortId}... normal exit, cleaning up`);
970
998
  this.stopTyping(session);
971
999
  if (session.updateTimer) {
972
1000
  clearTimeout(session.updateTimer);
@@ -984,8 +1012,14 @@ export class SessionManager {
984
1012
  this.postIndex.delete(postId);
985
1013
  }
986
1014
  }
987
- // Remove from persistence when session ends normally
988
- this.unpersistSession(threadId);
1015
+ // Only unpersist for normal exits (code 0 or null means graceful completion)
1016
+ // Non-zero exits might be recoverable, so we keep the session persisted
1017
+ if (code === 0 || code === null) {
1018
+ this.unpersistSession(threadId);
1019
+ }
1020
+ else {
1021
+ console.log(` [exit] Session ${shortId}... non-zero exit, preserving for potential retry`);
1022
+ }
989
1023
  console.log(` ■ Session ended (${shortId}…) — ${this.sessions.size} active`);
990
1024
  }
991
1025
  // ---------------------------------------------------------------------------
@@ -1011,10 +1045,16 @@ export class SessionManager {
1011
1045
  this.startTyping(session);
1012
1046
  }
1013
1047
  /** Kill a specific session */
1014
- killSession(threadId) {
1048
+ killSession(threadId, unpersist = true) {
1015
1049
  const session = this.sessions.get(threadId);
1016
1050
  if (!session)
1017
1051
  return;
1052
+ const shortId = threadId.substring(0, 8);
1053
+ // Set restarting flag to prevent handleExit from also unpersisting
1054
+ // (we'll do it explicitly here if requested)
1055
+ if (!unpersist) {
1056
+ session.isRestarting = true; // Reuse this flag to skip cleanup in handleExit
1057
+ }
1018
1058
  this.stopTyping(session);
1019
1059
  session.claude.kill();
1020
1060
  // Clean up session from maps
@@ -1024,7 +1064,10 @@ export class SessionManager {
1024
1064
  this.postIndex.delete(postId);
1025
1065
  }
1026
1066
  }
1027
- const shortId = threadId.substring(0, 8);
1067
+ // Explicitly unpersist if requested (e.g., for timeout, cancel, etc.)
1068
+ if (unpersist) {
1069
+ this.unpersistSession(threadId);
1070
+ }
1028
1071
  console.log(` ✖ Session killed (${shortId}…) — ${this.sessions.size} active`);
1029
1072
  }
1030
1073
  /** Cancel a session with user feedback */
@@ -1274,13 +1317,16 @@ export class SessionManager {
1274
1317
  }
1275
1318
  /** Kill all active sessions (for graceful shutdown) */
1276
1319
  killAllSessions() {
1277
- // Set shutdown flag to suppress exit messages
1320
+ console.log(` [shutdown] killAllSessions called, isShuttingDown already=${this.isShuttingDown}`);
1321
+ // Set shutdown flag to suppress exit messages (should already be true from setShuttingDown)
1278
1322
  this.isShuttingDown = true;
1279
1323
  const count = this.sessions.size;
1280
- for (const [, session] of this.sessions.entries()) {
1281
- this.stopTyping(session);
1282
- session.claude.kill();
1324
+ console.log(` [shutdown] About to kill ${count} session(s) (preserving persistence for resume)`);
1325
+ // Kill each session WITHOUT unpersisting - we want them to resume after restart
1326
+ for (const [threadId] of this.sessions.entries()) {
1327
+ this.killSession(threadId, false); // false = don't unpersist
1283
1328
  }
1329
+ // Maps should already be cleared by killSession, but clear again to be safe
1284
1330
  this.sessions.clear();
1285
1331
  this.postIndex.clear();
1286
1332
  if (this.cleanupTimer) {
@@ -1288,7 +1334,7 @@ export class SessionManager {
1288
1334
  this.cleanupTimer = null;
1289
1335
  }
1290
1336
  if (count > 0) {
1291
- console.log(` ✖ Killed ${count} session${count === 1 ? '' : 's'}`);
1337
+ console.log(` ✖ Killed ${count} session${count === 1 ? '' : 's'} (sessions preserved for resume)`);
1292
1338
  }
1293
1339
  }
1294
1340
  /** Cleanup idle sessions that have exceeded timeout */
package/dist/index.js CHANGED
@@ -211,6 +211,8 @@ async function main() {
211
211
  isShuttingDown = true;
212
212
  console.log('');
213
213
  console.log(` 👋 ${dim('Shutting down...')}`);
214
+ // Set shutdown flag FIRST to prevent race conditions with exit events
215
+ session.setShuttingDown();
214
216
  // Post shutdown message to active sessions
215
217
  const activeThreads = session.getActiveThreadIds();
216
218
  if (activeThreads.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mattermost-claude-code",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
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",