claude-flow
Version:
Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration
560 lines (500 loc) • 14.4 kB
text/typescript
/**
* V3 Session Management Hooks
*
* Provides session-end and session-restore hooks for state persistence.
* Enables cross-session memory and state recovery.
*
* @module v3/shared/hooks/session-hooks
*/
import {
HookEvent,
HookContext,
HookResult,
HookPriority,
SessionInfo,
} from './types.js';
import { HookRegistry } from './registry.js';
/**
* Session state to persist
*/
export interface SessionState {
/** Session ID */
sessionId: string;
/** Session start time */
startTime: Date;
/** Session end time */
endTime?: Date;
/** Working directory */
workingDirectory?: string;
/** Active tasks */
activeTasks?: Array<{
id: string;
description: string;
status: 'pending' | 'in_progress' | 'completed' | 'failed';
}>;
/** Spawned agents */
spawnedAgents?: Array<{
id: string;
type: string;
status: 'active' | 'idle' | 'terminated';
}>;
/** Memory entries */
memoryEntries?: Array<{
key: string;
namespace: string;
type: string;
}>;
/** Git state */
gitState?: {
branch: string;
uncommittedChanges: number;
lastCommit?: string;
};
/** Learning metrics */
learningMetrics?: {
patternsLearned: number;
trajectoryCount: number;
avgConfidence: number;
};
/** Custom metadata */
metadata?: Record<string, unknown>;
}
/**
* Session-end hook result
*/
export interface SessionEndHookResult extends HookResult {
/** Session state that was persisted */
persistedState?: SessionState;
/** File path where state was saved */
statePath?: string;
/** Duration of the session in ms */
duration?: number;
/** Summary of session activity */
summary?: SessionSummary;
}
/**
* Session-restore hook result
*/
export interface SessionRestoreHookResult extends HookResult {
/** Restored session state */
restoredState?: SessionState;
/** Number of tasks restored */
tasksRestored?: number;
/** Number of agents restored */
agentsRestored?: number;
/** Memory entries restored */
memoryRestored?: number;
/** Warnings during restoration */
warnings?: string[];
}
/**
* Session summary
*/
export interface SessionSummary {
/** Total tasks executed */
tasksExecuted: number;
/** Successful tasks */
tasksSucceeded: number;
/** Failed tasks */
tasksFailed: number;
/** Commands executed */
commandsExecuted: number;
/** Files modified */
filesModified: number;
/** Agents spawned */
agentsSpawned: number;
/** Duration in ms */
duration: number;
}
/**
* Session storage interface
*/
export interface SessionStorage {
/** Save session state */
save(sessionId: string, state: SessionState): Promise<void>;
/** Load session state */
load(sessionId: string): Promise<SessionState | null>;
/** List available sessions */
list(): Promise<Array<{ id: string; startTime: Date; summary?: SessionSummary }>>;
/** Delete session */
delete(sessionId: string): Promise<boolean>;
/** Get latest session ID */
getLatest(): Promise<string | null>;
}
/**
* In-memory session storage (for testing and fallback)
*/
export class InMemorySessionStorage implements SessionStorage {
private sessions: Map<string, SessionState> = new Map();
async save(sessionId: string, state: SessionState): Promise<void> {
this.sessions.set(sessionId, state);
}
async load(sessionId: string): Promise<SessionState | null> {
return this.sessions.get(sessionId) || null;
}
async list(): Promise<Array<{ id: string; startTime: Date; summary?: SessionSummary }>> {
return Array.from(this.sessions.entries()).map(([id, state]) => ({
id,
startTime: state.startTime,
}));
}
async delete(sessionId: string): Promise<boolean> {
return this.sessions.delete(sessionId);
}
async getLatest(): Promise<string | null> {
let latest: { id: string; time: number } | null = null;
for (const [id, state] of this.sessions) {
const time = state.startTime.getTime();
if (!latest || time > latest.time) {
latest = { id, time };
}
}
return latest?.id || null;
}
}
/**
* Session activity tracker
*/
interface SessionActivity {
tasksExecuted: number;
tasksSucceeded: number;
tasksFailed: number;
commandsExecuted: number;
filesModified: Set<string>;
agentsSpawned: Set<string>;
}
/**
* Session Hooks Manager
*
* Manages session lifecycle hooks with state persistence.
*/
export class SessionHooksManager {
private registry: HookRegistry;
private storage: SessionStorage;
private currentSessionId: string | null = null;
private sessionStartTime: Date | null = null;
private activity: SessionActivity = {
tasksExecuted: 0,
tasksSucceeded: 0,
tasksFailed: 0,
commandsExecuted: 0,
filesModified: new Set(),
agentsSpawned: new Set(),
};
constructor(registry: HookRegistry, storage?: SessionStorage) {
this.registry = registry;
this.storage = storage || new InMemorySessionStorage();
this.registerDefaultHooks();
}
/**
* Register default session hooks
*/
private registerDefaultHooks(): void {
// Session start hook
this.registry.register(
HookEvent.SessionStart,
this.handleSessionStart.bind(this),
HookPriority.High,
{ name: 'session-hooks:start' }
);
// Session end hook
this.registry.register(
HookEvent.SessionEnd,
this.handleSessionEnd.bind(this),
HookPriority.High,
{ name: 'session-hooks:end' }
);
// Session resume hook (for restoration)
this.registry.register(
HookEvent.SessionResume,
this.handleSessionResume.bind(this),
HookPriority.High,
{ name: 'session-hooks:resume' }
);
// Track tasks
this.registry.register(
HookEvent.PostTaskExecute,
this.trackTaskExecution.bind(this),
HookPriority.Low,
{ name: 'session-hooks:track-task' }
);
// Track commands
this.registry.register(
HookEvent.PostCommand,
this.trackCommandExecution.bind(this),
HookPriority.Low,
{ name: 'session-hooks:track-command' }
);
// Track file modifications
this.registry.register(
HookEvent.PostEdit,
this.trackFileModification.bind(this),
HookPriority.Low,
{ name: 'session-hooks:track-file' }
);
// Track agent spawns
this.registry.register(
HookEvent.PostAgentSpawn,
this.trackAgentSpawn.bind(this),
HookPriority.Low,
{ name: 'session-hooks:track-agent' }
);
}
/**
* Handle session start
*/
async handleSessionStart(context: HookContext): Promise<HookResult> {
this.currentSessionId = context.session?.id || `session-${Date.now()}`;
this.sessionStartTime = new Date();
this.resetActivity();
return {
success: true,
data: {
session: {
id: this.currentSessionId,
startTime: this.sessionStartTime,
},
},
};
}
/**
* Handle session end
*/
async handleSessionEnd(context: HookContext): Promise<SessionEndHookResult> {
if (!this.currentSessionId || !this.sessionStartTime) {
return { success: true }; // No active session to end
}
const endTime = new Date();
const duration = endTime.getTime() - this.sessionStartTime.getTime();
// Build session summary
const summary: SessionSummary = {
tasksExecuted: this.activity.tasksExecuted,
tasksSucceeded: this.activity.tasksSucceeded,
tasksFailed: this.activity.tasksFailed,
commandsExecuted: this.activity.commandsExecuted,
filesModified: this.activity.filesModified.size,
agentsSpawned: this.activity.agentsSpawned.size,
duration,
};
// Build session state
const state: SessionState = {
sessionId: this.currentSessionId,
startTime: this.sessionStartTime,
endTime,
workingDirectory: context.metadata?.workingDirectory as string | undefined,
activeTasks: context.metadata?.activeTasks as SessionState['activeTasks'],
spawnedAgents: context.metadata?.spawnedAgents as SessionState['spawnedAgents'],
memoryEntries: context.metadata?.memoryEntries as SessionState['memoryEntries'],
gitState: context.metadata?.gitState as SessionState['gitState'],
learningMetrics: context.metadata?.learningMetrics as SessionState['learningMetrics'],
metadata: {
summary,
...(context.metadata || {}),
},
};
// Persist state
await this.storage.save(this.currentSessionId, state);
// Reset session tracking
const sessionId = this.currentSessionId;
this.currentSessionId = null;
this.sessionStartTime = null;
this.resetActivity();
return {
success: true,
persistedState: state,
statePath: `sessions/${sessionId}.json`,
duration,
summary,
};
}
/**
* Handle session resume (restoration)
*/
async handleSessionResume(context: HookContext): Promise<SessionRestoreHookResult> {
let sessionId = context.session?.id;
// If 'latest' is requested, get the most recent session
if (sessionId === 'latest' || !sessionId) {
sessionId = await this.storage.getLatest() || undefined;
}
if (!sessionId) {
return {
success: false,
error: new Error('No session ID provided and no previous sessions found'),
warnings: ['No sessions available for restoration'],
};
}
// Load session state
const state = await this.storage.load(sessionId);
if (!state) {
return {
success: false,
error: new Error(`Session ${sessionId} not found`),
warnings: [`Session ${sessionId} does not exist or has been deleted`],
};
}
const warnings: string[] = [];
// Validate state age
const stateAge = Date.now() - state.startTime.getTime();
const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days
if (stateAge > maxAge) {
warnings.push(`Session is ${Math.floor(stateAge / (24 * 60 * 60 * 1000))} days old, some state may be stale`);
}
// Count restorable items
const tasksRestored = state.activeTasks?.length || 0;
const agentsRestored = state.spawnedAgents?.length || 0;
const memoryRestored = state.memoryEntries?.length || 0;
// Check for incomplete tasks
const incompleteTasks = state.activeTasks?.filter(
t => t.status === 'pending' || t.status === 'in_progress'
);
if (incompleteTasks && incompleteTasks.length > 0) {
warnings.push(`${incompleteTasks.length} tasks were incomplete when session ended`);
}
// Update current session tracking
this.currentSessionId = `session-${Date.now()}-restored`;
this.sessionStartTime = new Date();
this.resetActivity();
return {
success: true,
restoredState: state,
tasksRestored,
agentsRestored,
memoryRestored,
warnings: warnings.length > 0 ? warnings : undefined,
data: {
session: {
id: this.currentSessionId,
startTime: this.sessionStartTime,
metadata: {
restoredFrom: sessionId,
originalStartTime: state.startTime,
},
},
},
};
}
/**
* Track task execution
*/
private async trackTaskExecution(context: HookContext): Promise<HookResult> {
this.activity.tasksExecuted++;
if (context.metadata?.success !== false) {
this.activity.tasksSucceeded++;
} else {
this.activity.tasksFailed++;
}
return { success: true };
}
/**
* Track command execution
*/
private async trackCommandExecution(context: HookContext): Promise<HookResult> {
this.activity.commandsExecuted++;
return { success: true };
}
/**
* Track file modification
*/
private async trackFileModification(context: HookContext): Promise<HookResult> {
if (context.file?.path) {
this.activity.filesModified.add(context.file.path);
}
return { success: true };
}
/**
* Track agent spawn
*/
private async trackAgentSpawn(context: HookContext): Promise<HookResult> {
if (context.agent?.id) {
this.activity.agentsSpawned.add(context.agent.id);
}
return { success: true };
}
/**
* Reset activity tracking
*/
private resetActivity(): void {
this.activity = {
tasksExecuted: 0,
tasksSucceeded: 0,
tasksFailed: 0,
commandsExecuted: 0,
filesModified: new Set(),
agentsSpawned: new Set(),
};
}
/**
* Execute session-end hook manually
*/
async executeSessionEnd(metadata?: Record<string, unknown>): Promise<SessionEndHookResult> {
const context: HookContext = {
event: HookEvent.SessionEnd,
timestamp: new Date(),
session: this.currentSessionId
? {
id: this.currentSessionId,
startTime: this.sessionStartTime!,
}
: undefined,
metadata,
};
return this.handleSessionEnd(context);
}
/**
* Execute session-restore hook manually
*/
async executeSessionRestore(
sessionId?: string,
metadata?: Record<string, unknown>
): Promise<SessionRestoreHookResult> {
const context: HookContext = {
event: HookEvent.SessionResume,
timestamp: new Date(),
session: {
id: sessionId || 'latest',
startTime: new Date(),
},
metadata,
};
return this.handleSessionResume(context);
}
/**
* List available sessions
*/
async listSessions(): Promise<Array<{ id: string; startTime: Date; summary?: SessionSummary }>> {
return this.storage.list();
}
/**
* Delete a session
*/
async deleteSession(sessionId: string): Promise<boolean> {
return this.storage.delete(sessionId);
}
/**
* Get current session ID
*/
getCurrentSessionId(): string | null {
return this.currentSessionId;
}
/**
* Get current session activity
*/
getCurrentActivity(): SessionActivity {
return { ...this.activity };
}
/**
* Set storage backend
*/
setStorage(storage: SessionStorage): void {
this.storage = storage;
}
}
/**
* Create session hooks manager
*/
export function createSessionHooksManager(
registry: HookRegistry,
storage?: SessionStorage
): SessionHooksManager {
return new SessionHooksManager(registry, storage);
}