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 +12 -0
- package/dist/claude/session.d.ts +3 -1
- package/dist/claude/session.js +71 -25
- package/dist/index.js +2 -0
- package/package.json +1 -1
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
|
package/dist/claude/session.d.ts
CHANGED
|
@@ -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) */
|
package/dist/claude/session.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
965
|
-
|
|
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
|
-
|
|
969
|
-
|
|
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
|
-
//
|
|
988
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
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) {
|