mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +02:00
feat: add resumeSessionId support for conversation resume after reboot
Add resumeSessionId field throughout the session creation pipeline, allowing sessions to resume previous Claude conversations via --resume flag instead of --session-id.
This commit is contained in:
@@ -61,6 +61,8 @@ export interface CreateSessionOptions {
|
|||||||
claudeMode?: ClaudeMode;
|
claudeMode?: ClaudeMode;
|
||||||
allowedTools?: string;
|
allowedTools?: string;
|
||||||
openCodeConfig?: OpenCodeConfig;
|
openCodeConfig?: OpenCodeConfig;
|
||||||
|
/** When restoring after reboot, resume a previous Claude conversation by its session ID */
|
||||||
|
resumeSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Options for respawning a dead pane. */
|
/** Options for respawning a dead pane. */
|
||||||
|
|||||||
+8
-5
@@ -324,6 +324,7 @@ export class Session extends EventEmitter {
|
|||||||
|
|
||||||
// OpenCode configuration (only for mode === 'opencode')
|
// OpenCode configuration (only for mode === 'opencode')
|
||||||
private _openCodeConfig: OpenCodeConfig | undefined;
|
private _openCodeConfig: OpenCodeConfig | undefined;
|
||||||
|
private _resumeSessionId: string | undefined;
|
||||||
|
|
||||||
// Session color for visual differentiation
|
// Session color for visual differentiation
|
||||||
private _color: import('./types.js').SessionColor = 'default';
|
private _color: import('./types.js').SessionColor = 'default';
|
||||||
@@ -382,6 +383,8 @@ export class Session extends EventEmitter {
|
|||||||
allowedTools?: string;
|
allowedTools?: string;
|
||||||
/** OpenCode configuration (only for mode === 'opencode') */
|
/** OpenCode configuration (only for mode === 'opencode') */
|
||||||
openCodeConfig?: OpenCodeConfig;
|
openCodeConfig?: OpenCodeConfig;
|
||||||
|
/** Resume a previous Claude conversation (used after server reboot) */
|
||||||
|
resumeSessionId?: string;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
@@ -398,12 +401,10 @@ export class Session extends EventEmitter {
|
|||||||
this.createdAt = config.createdAt || Date.now();
|
this.createdAt = config.createdAt || Date.now();
|
||||||
this.mode = config.mode || 'claude';
|
this.mode = config.mode || 'claude';
|
||||||
this._name = config.name || '';
|
this._name = config.name || '';
|
||||||
|
this._resumeSessionId = config.resumeSessionId;
|
||||||
this._lastActivityAt = this.createdAt;
|
this._lastActivityAt = this.createdAt;
|
||||||
// Set claudeSessionId immediately — Codeman always passes --session-id ${this.id}
|
// Set claudeSessionId — when resuming, the Claude conversation ID is the resumed one.
|
||||||
// to Claude CLI, so the Claude session ID always matches the Codeman session ID.
|
this._claudeSessionId = config.resumeSessionId || this.id;
|
||||||
// This ensures subagent matching works even for recovered sessions (where
|
|
||||||
// startInteractive() hasn't been called yet).
|
|
||||||
this._claudeSessionId = this.id;
|
|
||||||
this._mux = config.mux || null;
|
this._mux = config.mux || null;
|
||||||
this._useMux = config.useMux ?? (this._mux !== null && this._mux.isAvailable());
|
this._useMux = config.useMux ?? (this._mux !== null && this._mux.isAvailable());
|
||||||
this._muxSession = config.muxSession || null;
|
this._muxSession = config.muxSession || null;
|
||||||
@@ -792,6 +793,7 @@ export class Session extends EventEmitter {
|
|||||||
cliAccountType: this._cliAccountType || undefined,
|
cliAccountType: this._cliAccountType || undefined,
|
||||||
cliLatestVersion: this._cliLatestVersion || undefined,
|
cliLatestVersion: this._cliLatestVersion || undefined,
|
||||||
openCodeConfig: this._openCodeConfig,
|
openCodeConfig: this._openCodeConfig,
|
||||||
|
resumeSessionId: this._resumeSessionId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -936,6 +938,7 @@ export class Session extends EventEmitter {
|
|||||||
claudeMode: this._claudeMode,
|
claudeMode: this._claudeMode,
|
||||||
allowedTools: this._allowedTools,
|
allowedTools: this._allowedTools,
|
||||||
openCodeConfig: this._openCodeConfig,
|
openCodeConfig: this._openCodeConfig,
|
||||||
|
resumeSessionId: this._resumeSessionId,
|
||||||
});
|
});
|
||||||
console.log('[Session] Created mux session:', this._muxSession.muxName);
|
console.log('[Session] Created mux session:', this._muxSession.muxName);
|
||||||
// No extra sleep — createSession() already waits for tmux readiness
|
// No extra sleep — createSession() already waits for tmux readiness
|
||||||
|
|||||||
+19
-2
@@ -200,12 +200,17 @@ function buildSpawnCommand(options: {
|
|||||||
claudeMode?: ClaudeMode;
|
claudeMode?: ClaudeMode;
|
||||||
allowedTools?: string;
|
allowedTools?: string;
|
||||||
openCodeConfig?: OpenCodeConfig;
|
openCodeConfig?: OpenCodeConfig;
|
||||||
|
resumeSessionId?: string;
|
||||||
}): string {
|
}): string {
|
||||||
if (options.mode === 'claude') {
|
if (options.mode === 'claude') {
|
||||||
// Validate model to prevent command injection
|
// Validate model to prevent command injection
|
||||||
const safeModel = options.model && /^[a-zA-Z0-9._-]+$/.test(options.model) ? options.model : undefined;
|
const safeModel = options.model && /^[a-zA-Z0-9._-]+$/.test(options.model) ? options.model : undefined;
|
||||||
const modelFlag = safeModel ? ` --model ${safeModel}` : '';
|
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') {
|
if (options.mode === 'opencode') {
|
||||||
return buildOpenCodeCommand(options.openCodeConfig);
|
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).
|
* In test mode: creates an in-memory session only (no real tmux session).
|
||||||
*/
|
*/
|
||||||
async createSession(options: CreateSessionOptions): Promise<MuxSession> {
|
async createSession(options: CreateSessionOptions): Promise<MuxSession> {
|
||||||
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)}`;
|
const muxName = `codeman-${sessionId.slice(0, 8)}`;
|
||||||
|
|
||||||
if (!isValidMuxName(muxName)) {
|
if (!isValidMuxName(muxName)) {
|
||||||
@@ -433,6 +449,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
|||||||
claudeMode,
|
claudeMode,
|
||||||
allowedTools,
|
allowedTools,
|
||||||
openCodeConfig,
|
openCodeConfig,
|
||||||
|
resumeSessionId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const config = niceConfig || DEFAULT_NICE_CONFIG;
|
const config = niceConfig || DEFAULT_NICE_CONFIG;
|
||||||
|
|||||||
@@ -143,6 +143,8 @@ export interface SessionState {
|
|||||||
cliLatestVersion?: string;
|
cliLatestVersion?: string;
|
||||||
/** OpenCode-specific configuration (only for mode === 'opencode') */
|
/** OpenCode-specific configuration (only for mode === 'opencode') */
|
||||||
openCodeConfig?: OpenCodeConfig;
|
openCodeConfig?: OpenCodeConfig;
|
||||||
|
/** Claude conversation session ID to resume after reboot (set by restore script) */
|
||||||
|
resumeSessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -125,6 +125,12 @@ export const CreateSessionSchema = z.object({
|
|||||||
name: z.string().max(100).optional(),
|
name: z.string().max(100).optional(),
|
||||||
envOverrides: safeEnvOverridesSchema,
|
envOverrides: safeEnvOverridesSchema,
|
||||||
openCodeConfig: OpenCodeConfigSchema,
|
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(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user