diff --git a/src/mux-interface.ts b/src/mux-interface.ts index 6007e472..cc3d5e60 100644 --- a/src/mux-interface.ts +++ b/src/mux-interface.ts @@ -61,6 +61,8 @@ export interface CreateSessionOptions { claudeMode?: ClaudeMode; allowedTools?: string; openCodeConfig?: OpenCodeConfig; + /** When restoring after reboot, resume a previous Claude conversation by its session ID */ + resumeSessionId?: string; } /** Options for respawning a dead pane. */ diff --git a/src/session.ts b/src/session.ts index 210f3eb4..8e6051ab 100644 --- a/src/session.ts +++ b/src/session.ts @@ -324,6 +324,7 @@ export class Session extends EventEmitter { // OpenCode configuration (only for mode === 'opencode') private _openCodeConfig: OpenCodeConfig | undefined; + private _resumeSessionId: string | undefined; // Session color for visual differentiation private _color: import('./types.js').SessionColor = 'default'; @@ -382,6 +383,8 @@ export class Session extends EventEmitter { allowedTools?: string; /** OpenCode configuration (only for mode === 'opencode') */ openCodeConfig?: OpenCodeConfig; + /** Resume a previous Claude conversation (used after server reboot) */ + resumeSessionId?: string; } ) { super(); @@ -398,12 +401,10 @@ export class Session extends EventEmitter { this.createdAt = config.createdAt || Date.now(); this.mode = config.mode || 'claude'; this._name = config.name || ''; + this._resumeSessionId = config.resumeSessionId; this._lastActivityAt = this.createdAt; - // Set claudeSessionId immediately — Codeman always passes --session-id ${this.id} - // to Claude CLI, so the Claude session ID always matches the Codeman session ID. - // This ensures subagent matching works even for recovered sessions (where - // startInteractive() hasn't been called yet). - this._claudeSessionId = this.id; + // Set claudeSessionId — when resuming, the Claude conversation ID is the resumed one. + this._claudeSessionId = config.resumeSessionId || this.id; this._mux = config.mux || null; this._useMux = config.useMux ?? (this._mux !== null && this._mux.isAvailable()); this._muxSession = config.muxSession || null; @@ -792,6 +793,7 @@ export class Session extends EventEmitter { cliAccountType: this._cliAccountType || undefined, cliLatestVersion: this._cliLatestVersion || undefined, openCodeConfig: this._openCodeConfig, + resumeSessionId: this._resumeSessionId, }; } @@ -936,6 +938,7 @@ export class Session extends EventEmitter { claudeMode: this._claudeMode, allowedTools: this._allowedTools, openCodeConfig: this._openCodeConfig, + resumeSessionId: this._resumeSessionId, }); console.log('[Session] Created mux session:', this._muxSession.muxName); // No extra sleep — createSession() already waits for tmux readiness diff --git a/src/tmux-manager.ts b/src/tmux-manager.ts index 1551649a..9dff494f 100644 --- a/src/tmux-manager.ts +++ b/src/tmux-manager.ts @@ -200,12 +200,17 @@ function buildSpawnCommand(options: { claudeMode?: ClaudeMode; allowedTools?: string; openCodeConfig?: OpenCodeConfig; + resumeSessionId?: string; }): string { if (options.mode === 'claude') { // Validate model to prevent command injection const safeModel = options.model && /^[a-zA-Z0-9._-]+$/.test(options.model) ? options.model : undefined; const modelFlag = safeModel ? ` --model ${safeModel}` : ''; - return `claude${buildClaudePermissionFlags(options.claudeMode, options.allowedTools)} --session-id "${options.sessionId}"${modelFlag}`; + // Use --resume to restore a previous conversation, otherwise --session-id for new sessions + const safeResumeId = + options.resumeSessionId && /^[a-f0-9-]+$/.test(options.resumeSessionId) ? options.resumeSessionId : undefined; + const sessionFlag = safeResumeId ? ` --resume "${safeResumeId}"` : ` --session-id "${options.sessionId}"`; + return `claude${buildClaudePermissionFlags(options.claudeMode, options.allowedTools)}${sessionFlag}${modelFlag}`; } if (options.mode === 'opencode') { return buildOpenCodeCommand(options.openCodeConfig); @@ -370,7 +375,18 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer { * In test mode: creates an in-memory session only (no real tmux session). */ async createSession(options: CreateSessionOptions): Promise { - const { sessionId, workingDir, mode, name, niceConfig, model, claudeMode, allowedTools, openCodeConfig } = options; + const { + sessionId, + workingDir, + mode, + name, + niceConfig, + model, + claudeMode, + allowedTools, + openCodeConfig, + resumeSessionId, + } = options; const muxName = `codeman-${sessionId.slice(0, 8)}`; if (!isValidMuxName(muxName)) { @@ -433,6 +449,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer { claudeMode, allowedTools, openCodeConfig, + resumeSessionId, }); const config = niceConfig || DEFAULT_NICE_CONFIG; diff --git a/src/types/session.ts b/src/types/session.ts index 69728070..01cf3584 100644 --- a/src/types/session.ts +++ b/src/types/session.ts @@ -143,6 +143,8 @@ export interface SessionState { cliLatestVersion?: string; /** OpenCode-specific configuration (only for mode === 'opencode') */ openCodeConfig?: OpenCodeConfig; + /** Claude conversation session ID to resume after reboot (set by restore script) */ + resumeSessionId?: string; } /** diff --git a/src/web/schemas.ts b/src/web/schemas.ts index 0cd8e588..6922dc15 100644 --- a/src/web/schemas.ts +++ b/src/web/schemas.ts @@ -125,6 +125,12 @@ export const CreateSessionSchema = z.object({ name: z.string().max(100).optional(), envOverrides: safeEnvOverridesSchema, openCodeConfig: OpenCodeConfigSchema, + /** Resume a previous Claude conversation by its session ID (used for reboot recovery) */ + resumeSessionId: z + .string() + .max(100) + .regex(/^[a-f0-9-]+$/, 'resumeSessionId must be a valid UUID') + .optional(), }); /**