mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 21:49:42 +02:00
feat: persist full session state to state.json for CLI visibility
The web server now persists all per-session settings (name, mode, auto-compact, auto-clear, respawn config, ralph state, tokens, cost) to state.json so the CLI status/session commands can display them. Also removes unused pendingStepConfirm property from respawn controller. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+33
-4
@@ -92,7 +92,25 @@ sessionCmd
|
||||
if (stoppedSessions.length > 0) {
|
||||
console.log(chalk.bold('\nStopped Sessions:'));
|
||||
for (const session of stoppedSessions) {
|
||||
console.log(` ${chalk.gray(session.id.slice(0, 8))} ${chalk.gray('stopped')} ${session.workingDir}`);
|
||||
const name = session.name ? ` (${session.name})` : '';
|
||||
console.log(` ${chalk.gray(session.id.slice(0, 8))} ${chalk.gray('stopped')}${name} ${session.workingDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Show active sessions from state (when web server manages them)
|
||||
const activeSessions = Object.values(stored).filter((s) => s.status !== 'stopped');
|
||||
if (sessions.length === 0 && activeSessions.length > 0) {
|
||||
console.log(chalk.bold('\nActive Sessions (from web server):'));
|
||||
for (const session of activeSessions) {
|
||||
const status = session.status === 'idle'
|
||||
? chalk.green('idle')
|
||||
: session.status === 'busy'
|
||||
? chalk.yellow('busy')
|
||||
: chalk.red(session.status);
|
||||
const name = session.name ? ` (${session.name})` : '';
|
||||
const mode = session.mode === 'shell' ? chalk.gray(' [shell]') : '';
|
||||
const cost = session.totalCost ? chalk.gray(` $${session.totalCost.toFixed(4)}`) : '';
|
||||
console.log(` ${chalk.cyan(session.id.slice(0, 8))} ${status}${name}${mode}${cost} ${session.workingDir}`);
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
@@ -371,16 +389,27 @@ program
|
||||
const loop = getRalphLoop();
|
||||
|
||||
const sessions = manager.getAllSessions();
|
||||
const stored = manager.getStoredSessions();
|
||||
const storedValues = Object.values(stored);
|
||||
const taskCounts = queue.getCount();
|
||||
const loopStatus = loop.status;
|
||||
|
||||
// Use live sessions if available, otherwise fall back to stored state
|
||||
const activeCount = sessions.length || storedValues.filter((s) => s.status !== 'stopped').length;
|
||||
const idleCount = sessions.length
|
||||
? sessions.filter((s) => s.isIdle()).length
|
||||
: storedValues.filter((s) => s.status === 'idle').length;
|
||||
const busyCount = sessions.length
|
||||
? sessions.filter((s) => s.isBusy()).length
|
||||
: storedValues.filter((s) => s.status === 'busy').length;
|
||||
|
||||
console.log(chalk.bold('\nClaudeman Status'));
|
||||
console.log('─'.repeat(40));
|
||||
|
||||
console.log(chalk.bold('\nSessions:'));
|
||||
console.log(` Active: ${sessions.length}`);
|
||||
console.log(` Idle: ${sessions.filter((s) => s.isIdle()).length}`);
|
||||
console.log(` Busy: ${sessions.filter((s) => s.isBusy()).length}`);
|
||||
console.log(` Active: ${activeCount}`);
|
||||
console.log(` Idle: ${idleCount}`);
|
||||
console.log(` Busy: ${busyCount}`);
|
||||
|
||||
console.log(chalk.bold('\nTasks:'));
|
||||
console.log(` Total: ${taskCounts.total}`);
|
||||
|
||||
@@ -413,9 +413,6 @@ export class RespawnController extends EventEmitter {
|
||||
/** Timer for step completion confirmation (waits for silence after completion) */
|
||||
private stepConfirmTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
/** Which step is pending confirmation */
|
||||
private pendingStepConfirm: 'update' | 'init' | 'kickstart' | null = null;
|
||||
|
||||
/** Fallback timeout for /clear step (ms) - sends /init without waiting for prompt */
|
||||
private static readonly CLEAR_FALLBACK_TIMEOUT_MS = 10000;
|
||||
|
||||
@@ -996,7 +993,7 @@ export class RespawnController extends EventEmitter {
|
||||
if (this.stepConfirmTimer) {
|
||||
clearTimeout(this.stepConfirmTimer);
|
||||
this.stepConfirmTimer = null;
|
||||
this.pendingStepConfirm = null;
|
||||
|
||||
}
|
||||
if (this.noOutputTimer) {
|
||||
clearTimeout(this.noOutputTimer);
|
||||
@@ -1112,7 +1109,6 @@ export class RespawnController extends EventEmitter {
|
||||
clearTimeout(this.stepConfirmTimer);
|
||||
}
|
||||
|
||||
this.pendingStepConfirm = step;
|
||||
this.log(`Step '${step}' completion detected, waiting ${this.config.completionConfirmMs}ms for silence...`);
|
||||
|
||||
this.stepConfirmTimer = setTimeout(() => {
|
||||
@@ -1121,7 +1117,7 @@ export class RespawnController extends EventEmitter {
|
||||
if (msSinceOutput >= this.config.completionConfirmMs) {
|
||||
this.log(`Step '${step}' confirmed: ${msSinceOutput}ms silence`);
|
||||
this.stepConfirmTimer = null;
|
||||
this.pendingStepConfirm = null;
|
||||
|
||||
|
||||
// Proceed with the step completion
|
||||
switch (step) {
|
||||
@@ -1152,7 +1148,6 @@ export class RespawnController extends EventEmitter {
|
||||
this.stepConfirmTimer = null;
|
||||
this.log(`Step confirmation cancelled (working detected)`);
|
||||
}
|
||||
this.pendingStepConfirm = null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -547,6 +547,18 @@ export class Session extends EventEmitter {
|
||||
currentTaskId: this._currentTaskId,
|
||||
createdAt: this.createdAt,
|
||||
lastActivityAt: this._lastActivityAt,
|
||||
name: this._name,
|
||||
mode: this.mode,
|
||||
autoClearEnabled: this._autoClearEnabled,
|
||||
autoClearThreshold: this._autoClearThreshold,
|
||||
autoCompactEnabled: this._autoCompactEnabled,
|
||||
autoCompactThreshold: this._autoCompactThreshold,
|
||||
autoCompactPrompt: this._autoCompactPrompt,
|
||||
totalCost: this._totalCost,
|
||||
inputTokens: this._totalInputTokens,
|
||||
outputTokens: this._totalOutputTokens,
|
||||
ralphEnabled: this._ralphTracker.enabled,
|
||||
ralphCompletionPhrase: this._ralphTracker.loopState.completionPhrase || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,32 @@ export interface SessionState {
|
||||
createdAt: number;
|
||||
/** Timestamp of last activity */
|
||||
lastActivityAt: number;
|
||||
/** Session display name */
|
||||
name?: string;
|
||||
/** Session mode: 'claude' or 'shell' */
|
||||
mode?: 'claude' | 'shell';
|
||||
/** Auto-clear enabled */
|
||||
autoClearEnabled?: boolean;
|
||||
/** Auto-clear token threshold */
|
||||
autoClearThreshold?: number;
|
||||
/** Auto-compact enabled */
|
||||
autoCompactEnabled?: boolean;
|
||||
/** Auto-compact token threshold */
|
||||
autoCompactThreshold?: number;
|
||||
/** Auto-compact prompt */
|
||||
autoCompactPrompt?: string;
|
||||
/** Total cost in USD */
|
||||
totalCost?: number;
|
||||
/** Input tokens used */
|
||||
inputTokens?: number;
|
||||
/** Output tokens used */
|
||||
outputTokens?: number;
|
||||
/** Respawn controller config (if enabled) */
|
||||
respawnConfig?: RespawnConfig & { durationMinutes?: number };
|
||||
/** Ralph / Todo tracker enabled */
|
||||
ralphEnabled?: boolean;
|
||||
/** Ralph completion phrase (if set) */
|
||||
ralphCompletionPhrase?: string;
|
||||
}
|
||||
|
||||
// ========== Task Types ==========
|
||||
@@ -171,6 +197,10 @@ export interface RespawnConfig {
|
||||
sendInit: boolean;
|
||||
/** Optional prompt to send if /init doesn't trigger work */
|
||||
kickstartPrompt?: string;
|
||||
/** Time to wait after completion message before confirming idle (ms) */
|
||||
completionConfirmMs?: number;
|
||||
/** Fallback timeout when no output received at all (ms) */
|
||||
noOutputTimeoutMs?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+50
-3
@@ -225,6 +225,7 @@ export class WebServer extends EventEmitter {
|
||||
});
|
||||
|
||||
this.sessions.set(session.id, session);
|
||||
this.persistSessionState(session);
|
||||
this.setupSessionListeners(session);
|
||||
|
||||
this.broadcast('session:created', session.toDetailedState());
|
||||
@@ -244,6 +245,7 @@ export class WebServer extends EventEmitter {
|
||||
session.name = body.name || '';
|
||||
// Also update the screen name if this session has a screen
|
||||
this.screenManager.updateScreenName(id, session.name);
|
||||
this.persistSessionState(session);
|
||||
this.broadcast('session:updated', session.toDetailedState());
|
||||
return { success: true, name: session.name };
|
||||
});
|
||||
@@ -392,7 +394,8 @@ export class WebServer extends EventEmitter {
|
||||
todoExpirationMinutes: todoExpirationMinutes || 60
|
||||
};
|
||||
|
||||
// Broadcast the update
|
||||
// Persist and broadcast the update
|
||||
this.persistSessionState(session);
|
||||
this.broadcast('session:ralphLoopUpdate', {
|
||||
sessionId: id,
|
||||
state: session.ralphLoopState
|
||||
@@ -600,8 +603,9 @@ export class WebServer extends EventEmitter {
|
||||
|
||||
controller.start();
|
||||
|
||||
// Persist respawn config to screen session
|
||||
// Persist respawn config to screen session and state.json
|
||||
this.saveRespawnConfig(id, controller.getConfig());
|
||||
this.persistSessionState(session);
|
||||
|
||||
this.broadcast('respawn:started', { sessionId: id, status: controller.getStatus() });
|
||||
|
||||
@@ -622,6 +626,12 @@ export class WebServer extends EventEmitter {
|
||||
// Clear persisted respawn config
|
||||
this.screenManager.clearRespawnConfig(id);
|
||||
|
||||
// Update state.json (respawnConfig removed)
|
||||
const session = this.sessions.get(id);
|
||||
if (session) {
|
||||
this.persistSessionState(session);
|
||||
}
|
||||
|
||||
this.broadcast('respawn:stopped', { sessionId: id });
|
||||
|
||||
return { success: true };
|
||||
@@ -641,6 +651,10 @@ export class WebServer extends EventEmitter {
|
||||
|
||||
// Persist updated config
|
||||
this.saveRespawnConfig(id, controller.getConfig());
|
||||
const session = this.sessions.get(id);
|
||||
if (session) {
|
||||
this.persistSessionState(session);
|
||||
}
|
||||
|
||||
this.broadcast('respawn:configUpdated', { sessionId: id, config: controller.getConfig() });
|
||||
|
||||
@@ -681,6 +695,9 @@ export class WebServer extends EventEmitter {
|
||||
this.setupTimedRespawn(id, body.durationMinutes);
|
||||
}
|
||||
|
||||
// Persist full session state with respawn config
|
||||
this.persistSessionState(session);
|
||||
|
||||
this.broadcast('respawn:started', { sessionId: id, status: controller.getStatus() });
|
||||
|
||||
return {
|
||||
@@ -727,8 +744,9 @@ export class WebServer extends EventEmitter {
|
||||
this.setupTimedRespawn(id, body.durationMinutes);
|
||||
}
|
||||
|
||||
// Persist respawn config to screen session
|
||||
// Persist respawn config to screen session and state.json
|
||||
this.saveRespawnConfig(id, controller.getConfig(), body?.durationMinutes);
|
||||
this.persistSessionState(session);
|
||||
|
||||
this.broadcast('respawn:started', { sessionId: id, status: controller.getStatus() });
|
||||
|
||||
@@ -758,6 +776,7 @@ export class WebServer extends EventEmitter {
|
||||
}
|
||||
|
||||
session.setAutoClear(body.enabled, body.threshold);
|
||||
this.persistSessionState(session);
|
||||
this.broadcast('session:updated', session.toDetailedState());
|
||||
|
||||
return {
|
||||
@@ -790,6 +809,7 @@ export class WebServer extends EventEmitter {
|
||||
}
|
||||
|
||||
session.setAutoCompact(body.enabled, body.threshold, body.prompt);
|
||||
this.persistSessionState(session);
|
||||
this.broadcast('session:updated', session.toDetailedState());
|
||||
|
||||
return {
|
||||
@@ -820,6 +840,7 @@ export class WebServer extends EventEmitter {
|
||||
|
||||
const session = new Session({ workingDir: dir });
|
||||
this.sessions.set(session.id, session);
|
||||
this.persistSessionState(session);
|
||||
this.setupSessionListeners(session);
|
||||
|
||||
this.broadcast('session:created', session.toDetailedState());
|
||||
@@ -1101,6 +1122,7 @@ export class WebServer extends EventEmitter {
|
||||
}
|
||||
|
||||
this.sessions.set(session.id, session);
|
||||
this.persistSessionState(session);
|
||||
this.setupSessionListeners(session);
|
||||
this.broadcast('session:created', session.toDetailedState());
|
||||
|
||||
@@ -1217,6 +1239,21 @@ export class WebServer extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
/** Persists full session state including respawn config to state.json */
|
||||
private persistSessionState(session: Session): void {
|
||||
const state = session.toState();
|
||||
const controller = this.respawnControllers.get(session.id);
|
||||
if (controller) {
|
||||
const config = controller.getConfig();
|
||||
const timerInfo = this.respawnTimers.get(session.id);
|
||||
const durationMinutes = timerInfo
|
||||
? Math.round((timerInfo.endAt - timerInfo.startedAt) / 60000)
|
||||
: undefined;
|
||||
state.respawnConfig = { ...config, durationMinutes };
|
||||
}
|
||||
this.store.setSession(session.id, state);
|
||||
}
|
||||
|
||||
// Helper to save respawn config to screen session for persistence
|
||||
private saveRespawnConfig(sessionId: string, config: RespawnConfig, durationMinutes?: number): void {
|
||||
const persistedConfig: PersistedRespawnConfig = {
|
||||
@@ -1311,6 +1348,7 @@ export class WebServer extends EventEmitter {
|
||||
session.removeAllListeners();
|
||||
await session.stop(killScreen);
|
||||
this.sessions.delete(sessionId);
|
||||
this.store.removeSession(sessionId);
|
||||
}
|
||||
|
||||
this.broadcast('session:deleted', { id: sessionId });
|
||||
@@ -1343,6 +1381,7 @@ export class WebServer extends EventEmitter {
|
||||
session.on('completion', (result, cost) => {
|
||||
this.broadcast('session:completion', { id: session.id, result, cost });
|
||||
this.broadcast('session:updated', session.toDetailedState());
|
||||
this.persistSessionState(session);
|
||||
});
|
||||
|
||||
session.on('exit', (code) => {
|
||||
@@ -1350,6 +1389,7 @@ export class WebServer extends EventEmitter {
|
||||
try {
|
||||
this.broadcast('session:exit', { id: session.id, code });
|
||||
this.broadcast('session:updated', session.toDetailedState());
|
||||
this.persistSessionState(session);
|
||||
} catch (err) {
|
||||
console.error(`[Server] Error broadcasting session exit for ${session.id}:`, err);
|
||||
}
|
||||
@@ -1478,6 +1518,11 @@ export class WebServer extends EventEmitter {
|
||||
this.broadcast('respawn:stopped', { sessionId, reason: 'duration_expired' });
|
||||
}
|
||||
this.respawnTimers.delete(sessionId);
|
||||
// Update persisted state (respawn no longer active)
|
||||
const session = this.sessions.get(sessionId);
|
||||
if (session) {
|
||||
this.persistSessionState(session);
|
||||
}
|
||||
}, durationMinutes * 60 * 1000);
|
||||
|
||||
this.respawnTimers.set(sessionId, { timer, endAt, startedAt: now });
|
||||
@@ -1557,6 +1602,7 @@ export class WebServer extends EventEmitter {
|
||||
// Create a session for this iteration
|
||||
session = new Session({ workingDir: run.workingDir });
|
||||
this.sessions.set(session.id, session);
|
||||
this.persistSessionState(session);
|
||||
this.setupSessionListeners(session);
|
||||
run.sessionId = session.id;
|
||||
|
||||
@@ -1884,6 +1930,7 @@ export class WebServer extends EventEmitter {
|
||||
});
|
||||
|
||||
this.sessions.set(session.id, session);
|
||||
this.persistSessionState(session);
|
||||
this.setupSessionListeners(session);
|
||||
|
||||
// Restore Ralph tracking state (Ralph Wiggum settings) if it was saved
|
||||
|
||||
Reference in New Issue
Block a user