import { tmpdir, homedir } from 'os';
import { join } from 'path';
import { writeFileSync, readFileSync, existsSync, unlinkSync, mkdirSync, readdirSync, watch, FSWatcher, statSync } from 'fs';
import { execSync } from 'child_process';
import { randomUUID } from 'crypto';
import type { ClaudeInstance, InterClaudeMessage } from './types.js';
export class SessionManager {
private baseDir: string;
private claudeDir: string;
private instanceFile: string;
private sessionData: ClaudeInstance;
private messageHandlers = new Map<string, (message: InterClaudeMessage) => void>();
private messageMailbox: InterClaudeMessage[] = []; // In-memory mailbox
private messageWatcher: FSWatcher | null = null;
private lastHeartbeat = 0;
private discoveryCache: { instances: ClaudeInstance[], timestamp: number } | null = null;
private learnedContext: Map<number, any> = new Map(); // Store rich context from other Claudes
constructor() {
// Ultra-minimal async message broker - single directory, rich context
const tempDir = tmpdir() || '/tmp';
this.baseDir = join(tempDir, 'claude-senator');
this.claudeDir = join(homedir(), '.claude');
// Track the parent Claude process, not the MCP server process
const parentPid = process.ppid || process.pid;
this.instanceFile = join(this.baseDir, `claude-${parentPid}.json`);
this.sessionData = {
pid: parentPid, // Use parent Claude PID, not MCP server PID
cwd: process.cwd(),
socketPath: '', // Not used in file-based approach
startTime: Date.now(),
lastSeen: Date.now(),
status: 'active',
projectPath: this.detectProjectPath(),
};
// Cleanup on exit
process.on('exit', () => this.cleanup());
process.on('SIGINT', () => this.cleanup());
process.on('SIGTERM', () => this.cleanup());
}
ensureInitialized(): void {
this.ensureDirectories();
this.registerSession();
}
async start(): Promise<void> {
try {
console.error(`[Claude Senator] Starting high-speed session (PID: ${process.pid})`);
this.ensureDirectories();
this.registerSession();
this.cleanupOldData();
this.startMessageWatching();
console.error(`[Claude Senator] Session registered with file watching (PID: ${process.pid})`);
} catch (error) {
console.error('[Claude Senator] Start error:', error);
throw error;
}
}
private ensureDirectories(): void {
try {
if (!existsSync(this.baseDir)) {
mkdirSync(this.baseDir, { recursive: true });
console.error(`[Claude Senator] Created ultra-minimal message directory: ${this.baseDir}`);
}
} catch (error) {
console.error('[Claude Senator] Failed to create directory:', error);
}
}
private startMessageWatching(): void {
try {
// Event-driven message checking - only on tool calls, no polling
// This keeps overhead minimal while ensuring messages are discovered
console.error(`[Claude Senator] Event-driven message checking enabled (PID: ${process.pid})`);
} catch (error) {
console.error('[Claude Senator] Failed to setup message checking:', error);
}
}
private syncWithOtherClaudes(): void {
try {
const instances = this.discoverInstances();
// Broadcast status periodically (every ~30 seconds)
const now = Date.now();
if (now - (this.lastStatusBroadcast || 0) > 30000) {
this.broadcastCurrentStatus();
this.lastStatusBroadcast = now;
}
// Update our knowledge of other Claude activities
instances.forEach(instance => {
if (instance.pid !== (process.ppid || process.pid)) {
const activity = this.parseLiveActivity(instance.pid);
if (activity.lastActivity > Date.now() - 10000) { // Active in last 10 seconds
// This Claude is actively working
console.error(`[Claude Senator] Active Claude detected: ${instance.pid} - ${activity.task}`);
}
}
});
} catch (error) {
// Silent fail - sync is best effort
}
}
private lastStatusBroadcast: number = 0;
private async broadcastCurrentStatus(): Promise<void> {
try {
const context = this.generateContextSummary();
const instances = this.discoverInstances();
// Only broadcast if there are other Claude instances
if (instances.length > 1) {
const statusMessage: InterClaudeMessage = {
id: randomUUID(),
from: process.ppid || process.pid,
type: 'shared_status',
content: `Status update: ${context.summary}`,
options: {
context: {
recentWork: context.recentWork,
activeFiles: context.activeFiles,
workingDir: context.workingDir,
timestamp: Date.now(),
autoGenerated: true
}
},
timestamp: Date.now()
};
await this.broadcastMessage(statusMessage);
console.error(`[Claude Senator] Broadcasted status to ${instances.length - 1} other Claude instances`);
}
} catch (error) {
console.error('[Claude Senator] Error broadcasting status:', error);
}
}
// Method to add messages directly to in-memory mailbox (Claude-to-Claude bus)
addToMailbox(message: InterClaudeMessage): void {
try {
const parentPid = process.ppid || process.pid;
// Only add messages addressed to us or broadcasts
if (!message.to || message.to === parentPid) {
this.messageMailbox.push(message);
this.handleIncomingMessage(message);
console.error(`[Claude Senator] Message added to mailbox from ${message.from}: ${message.content}`);
}
} catch (error) {
console.error('[Claude Senator] Failed to add message to mailbox:', error);
}
}
// Command injection now handled via rich messages, no polling needed
private registerSession(): void {
try {
this.sessionData.lastSeen = Date.now();
writeFileSync(this.instanceFile, JSON.stringify(this.sessionData, null, 2));
this.lastHeartbeat = Date.now();
console.error(`[Claude Senator] Instance registered: ${this.instanceFile}`);
} catch (error) {
console.error('[Claude Senator] Failed to register session:', error);
}
}
private updateHeartbeatIfNeeded(): void {
const now = Date.now();
if (now - this.lastHeartbeat > 30000) { // 30 seconds
this.registerSession();
}
}
private cleanupOldData(): void {
try {
const now = Date.now();
const messageMaxAge = 3600000; // 1 hour
const activePids = this.getActivePids();
if (!existsSync(this.baseDir)) return;
const files = readdirSync(this.baseDir);
// Clean up dead instance files
const instanceFiles = files.filter(f => f.startsWith('claude-') && f.endsWith('.json'));
for (const file of instanceFiles) {
const pidMatch = file.match(/claude-(\d+)\.json/);
if (pidMatch) {
const pid = parseInt(pidMatch[1]);
if (!activePids.includes(pid)) {
const filePath = join(this.baseDir, file);
try {
unlinkSync(filePath);
console.error(`[Claude Senator] Cleaned up dead instance: ${pid}`);
} catch (error) {
// Ignore cleanup errors
}
}
}
}
// Clean up old message files
const messageFiles = files.filter(f => !f.startsWith('claude-') && f.endsWith('.json'));
for (const file of messageFiles) {
const filePath = join(this.baseDir, file);
try {
const stats = statSync(filePath);
if ((now - stats.mtime.getTime()) > messageMaxAge) {
unlinkSync(filePath);
}
} catch (error) {
// Ignore cleanup errors
}
}
} catch (error) {
console.error('[Claude Senator] Cleanup error:', error);
}
}
private getActivePids(): number[] {
try {
const output = execSync('ps aux', { encoding: 'utf8' });
const lines = output.split('\n');
// More specific filtering - only match actual Claude binary processes
const claudeProcesses = lines.filter(line => {
const trimmed = line.trim();
return (
trimmed.includes(' claude ') || // Exact match with spaces
trimmed.includes(' claude\t') || // Tab-separated
trimmed.endsWith(' claude') // Ends with claude
) &&
!trimmed.includes('grep') &&
!trimmed.includes('claude-senator') &&
!trimmed.includes('claude-historian') &&
!trimmed.includes('terminal-notifier'); // Exclude notifications
});
console.error(`[Claude Senator] Found ${claudeProcesses.length} Claude processes`);
return claudeProcesses.map(line => {
const parts = line.trim().split(/\s+/);
const pid = parseInt(parts[1]);
console.error(`[Claude Senator] Claude process PID: ${pid}`);
return pid;
}).filter(pid => !isNaN(pid));
} catch (error) {
console.error('[Claude Senator] Failed to get active PIDs:', error);
return [];
}
}
private detectProjectPath(): string {
// Simple project detection - look for common project indicators
const cwd = process.cwd();
const indicators = ['package.json', 'pyproject.toml', 'Cargo.toml', '.git'];
for (const indicator of indicators) {
if (existsSync(join(cwd, indicator))) {
return cwd;
}
}
return cwd;
}
parseLiveActivity(pid: number): any {
try {
// Find the Claude instance data
const instancePath = join(this.baseDir, `claude-${pid}.json`);
if (!existsSync(instancePath)) {
return { pid, status: 'unknown', task: 'No instance data', files: [], lastActivity: 0 };
}
const instanceData = JSON.parse(readFileSync(instancePath, 'utf8'));
const workingDir = instanceData.cwd || instanceData.projectPath || '/unknown';
// Ensure workingDir is a string before calling replace()
const safeWorkingDir = String(workingDir || '/unknown');
// Encode working directory path for ~/.claude/projects lookup
const encodedPath = safeWorkingDir.replace(/\//g, '-').replace(/^-/, '');
const projectsPath = join(homedir(), '.claude', 'projects', encodedPath);
if (!existsSync(projectsPath)) {
return {
pid,
status: 'active',
task: `Working in ${workingDir}`,
files: [],
lastActivity: instanceData.lastSeen
};
}
// Find most recent conversation file
const conversationFiles = readdirSync(projectsPath)
.filter(f => f.endsWith('.jsonl'))
.map(f => {
const fullPath = join(projectsPath, f);
const stats = statSync(fullPath);
return { file: f, path: fullPath, mtime: stats.mtime.getTime() };
})
.sort((a, b) => b.mtime - a.mtime);
if (conversationFiles.length === 0) {
return {
pid,
status: 'active',
task: 'No recent conversation',
files: [],
lastActivity: instanceData.lastSeen
};
}
// Parse last few messages from most recent conversation
const recentFile = conversationFiles[0];
// execSync already imported
try {
// Get last 3 lines quickly with tail
const lastLines = execSync(`tail -3 "${recentFile.path}"`, { encoding: 'utf8', timeout: 1000 });
const lines = lastLines.trim().split('\n');
let currentTask = 'Active';
let recentFiles: string[] = [];
// Parse messages for current activity
for (const line of lines.reverse()) {
if (!line.trim()) continue;
try {
const message = JSON.parse(line);
const content = this.extractContentFromMessage(message);
if (content) {
// Extract current task from assistant messages
if (message.type === 'assistant' && content.includes('I')) {
const taskMatch = content.match(/I(?:'ll|'m| will| am)\s+([^.!?]*)/i);
if (taskMatch) {
currentTask = taskMatch[1].slice(0, 80);
break;
}
}
// Extract file references
const fileMatches = content.match(/[\w\-/\\.]+\.(ts|js|json|md|py|java|cpp|c|h|css|html|yml|yaml|toml|rs|go)/gi);
if (fileMatches) {
recentFiles.push(...fileMatches.slice(0, 3));
}
}
} catch (e) {
// Skip malformed lines
}
}
return {
pid,
status: 'active',
task: currentTask,
files: [...new Set(recentFiles)].slice(0, 3),
lastActivity: recentFile.mtime,
workingDir
};
} catch (e) {
return {
pid,
status: 'active',
task: 'Recent activity',
files: [],
lastActivity: recentFile.mtime,
workingDir
};
}
} catch (error) {
console.error(`[Claude Senator] Failed to parse activity for PID ${pid}:`, error);
return { pid, status: 'error', task: 'Parse failed', files: [], lastActivity: 0 };
}
}
private extractContentFromMessage(message: any): string {
try {
if (message.message?.content) {
if (typeof message.message.content === 'string') {
return message.message.content;
}
if (Array.isArray(message.message.content)) {
return message.message.content
.filter((item: any) => item.type === 'text')
.map((item: any) => item.text)
.join(' ');
}
}
return '';
} catch (e) {
return '';
}
}
getIncomingMessages(): InterClaudeMessage[] {
const messages: InterClaudeMessage[] = [];
try {
this.ensureDirectories();
if (!existsSync(this.baseDir)) {
return messages;
}
const messageFiles = readdirSync(this.baseDir).filter(f => f.endsWith('.json') && !f.startsWith('claude-'));
const currentPid = process.ppid || process.pid;
for (const file of messageFiles) {
try {
const filePath = join(this.baseDir, file);
const content = readFileSync(filePath, 'utf8');
const message = JSON.parse(content);
// Include messages for us, broadcasts, or responses to our queries
if (!message.to || message.to === currentPid || message.to === 'all' || message.from === currentPid) {
message._filePath = filePath;
messages.push(message);
}
} catch (e) {
console.error(`[Claude Senator] Error reading message file ${file}:`, e);
}
}
return messages.slice(-20); // Last 20 messages
} catch (error) {
console.error('[Claude Senator] Error reading incoming messages:', error);
return [];
}
}
markMessagesAsRead(messages: InterClaudeMessage[]): void {
try {
// Clean up ALL message files when any message is read (per user requirement)
this.cleanupAllMessages();
// Send acknowledgment messages with rich context to all senders
const uniqueSenders = [...new Set(messages.map(msg => msg.from))];
uniqueSenders.forEach(senderPid => {
this.sendContextualAcknowledgment(senderPid, messages.filter(msg => msg.from === senderPid));
});
// Update our status after processing messages
this.updateStatus('active', 'Processed messages and sent acknowledgments');
console.error(`[Claude Senator] Marked ${messages.length} messages as read with acknowledgments`);
} catch (error) {
console.error('[Claude Senator] Error cleaning up messages:', error);
}
}
private cleanupAllMessages(): void {
try {
if (!existsSync(this.baseDir)) return;
// Only clean up old messages (older than 5 minutes) to prevent message loss
const now = Date.now();
const messageFiles = readdirSync(this.baseDir).filter(f => !f.startsWith('claude-') && f.endsWith('.json'));
let cleanedCount = 0;
messageFiles.forEach(file => {
const filePath = join(this.baseDir, file);
try {
const stats = statSync(filePath);
const fileAge = now - stats.mtime.getTime();
// Only delete messages older than 5 minutes
if (fileAge > 5 * 60 * 1000) {
unlinkSync(filePath);
cleanedCount++;
console.error(`[Claude Senator] Cleaned up old message file: ${file}`);
}
} catch (error) {
// Ignore cleanup errors for individual files
}
});
console.error(`[Claude Senator] Cleaned up ${cleanedCount} old message files (kept recent ones)`);
} catch (error) {
console.error('[Claude Senator] Error during message cleanup:', error);
}
}
private async sendContextualAcknowledgment(senderPid: number, receivedMessages: InterClaudeMessage[]): Promise<void> {
try {
// Generate rich context summary
const context = this.generateContextSummary();
const messageCount = receivedMessages.length;
const acknowledgmentMessage: InterClaudeMessage = {
id: randomUUID(),
from: process.ppid || process.pid,
to: senderPid,
type: 'status',
content: `📋 Message${messageCount > 1 ? 's' : ''} received and processed. Current context: ${context.summary}`,
options: {
context: {
recentWork: context.recentWork,
currentTask: context.currentTask,
activeFiles: context.activeFiles,
workingDir: context.workingDir,
timestamp: Date.now()
}
},
timestamp: Date.now()
};
await this.sendMessage(senderPid, acknowledgmentMessage.content);
console.error(`[Claude Senator] Sent contextual acknowledgment to PID ${senderPid}`);
} catch (error) {
console.error(`[Claude Senator] Failed to send acknowledgment to PID ${senderPid}:`, error);
}
}
private generateContextSummary(): {
summary: string;
recentWork: string;
currentTask: string;
activeFiles: string[];
workingDir: string;
} {
try {
const activity = this.parseLiveActivity(process.ppid || process.pid);
const workingDir = process.cwd();
const projectName = workingDir.split('/').pop() || 'unknown';
return {
summary: `Working on ${activity.task} in ${projectName}`,
recentWork: activity.task,
currentTask: this.sessionData.currentTask || activity.task,
activeFiles: activity.files || [],
workingDir: workingDir
};
} catch (error) {
console.error('[Claude Senator] Error generating context summary:', error);
return {
summary: 'Active session',
recentWork: 'Recent activity',
currentTask: 'Working',
activeFiles: [],
workingDir: process.cwd()
};
}
}
private handleIncomingMessage(message: InterClaudeMessage): void {
// Update last seen
this.sessionData.lastSeen = Date.now();
this.registerSession();
// Handle different message types
switch (message.type) {
case 'ping':
this.sendMessage(message.from, 'pong');
break;
case 'status':
if (message.content === 'pong') {
// Update connection status
this.updateInstanceStatus(message.from, 'active');
}
break;
case 'shared_status':
// Log shared status message for pickup
console.log(`📬 Message from Claude ${message.from}: ${message.content}`);
break;
default:
// Delegate to registered handlers
const handler = this.messageHandlers.get(message.type);
if (handler) {
handler(message);
} else {
console.error(`[Claude Senator] No handler for message type: ${message.type}`);
}
break;
}
}
registerMessageHandler(type: string, handler: (message: InterClaudeMessage) => void): void {
this.messageHandlers.set(type, handler);
}
private createRichMessage(message: InterClaudeMessage): any {
const conversationPath = this.findCurrentConversationPath();
const currentContext = this.generateContextSummary();
return {
humanMessage: message.content,
from: process.ppid || process.pid,
to: message.to,
timestamp: Date.now(),
claudeContext: {
// Lookup keys for rich context (no duplication)
conversationPath: conversationPath,
lookupKeys: this.generateLookupKeys(message.content),
// Current state snapshot (minimal)
currentSnapshot: {
task: currentContext.currentTask,
files: currentContext.activeFiles.slice(0, 3),
workingDir: currentContext.workingDir,
project: currentContext.workingDir.split('/').pop()
},
// Collaboration intelligence
collaborationHints: {
needsHelp: this.extractNeedsHelp(),
canShare: this.extractCanShare(),
warnings: this.extractWarnings(),
urgency: message.options?.urgent ? 'high' : 'normal'
},
// Context pointers (not data)
contextPointers: {
recentJSONL: conversationPath,
projectPath: this.detectProjectPath(),
claudeDir: this.claudeDir
}
}
};
}
private extractRecentJSONLContext(): any[] {
try {
const workingDir = process.cwd();
const encodedPath = workingDir.replace(/\//g, '-').replace(/^-/, '');
const projectPath = join(homedir(), '.claude', 'projects', encodedPath);
if (!existsSync(projectPath)) {
return [];
}
// Find most recent JSONL conversation file
const conversationFiles = readdirSync(projectPath)
.filter(f => f.endsWith('.jsonl'))
.map(f => {
const fullPath = join(projectPath, f);
const stats = statSync(fullPath);
return { file: f, path: fullPath, mtime: stats.mtime.getTime() };
})
.sort((a, b) => b.mtime - a.mtime);
if (conversationFiles.length === 0) {
return [];
}
// Read recent messages from most recent file
const recentFile = conversationFiles[0];
// execSync already imported
// Get last 10 lines efficiently
const lastLines = execSync(`tail -10 "${recentFile.path}"`, { encoding: 'utf8', timeout: 2000 });
const lines = lastLines.trim().split('\n').filter((line: string) => line.trim());
return lines.map((line: string) => {
try {
return JSON.parse(line);
} catch (e) {
return null;
}
}).filter(Boolean);
} catch (error) {
console.error('[Claude Senator] Error extracting JSONL context:', error);
return [];
}
}
private extractRecentToolUsage(): string[] {
try {
// Lazy loading - only extract when actually needed for messages
return ['Read', 'Edit', 'Bash']; // Simple fallback to avoid overhead
} catch (error) {
return [];
}
}
private findCurrentConversationPath(): string {
try {
const workingDir = process.cwd();
const encodedPath = workingDir.replace(/\//g, '-').replace(/^-/, '');
const projectPath = join(this.claudeDir, 'projects', encodedPath);
if (!existsSync(projectPath)) {
return '';
}
const conversationFiles = readdirSync(projectPath)
.filter(f => f.endsWith('.jsonl'))
.map(f => {
const fullPath = join(projectPath, f);
const stats = statSync(fullPath);
return { file: f, path: fullPath, mtime: stats.mtime.getTime() };
})
.sort((a, b) => b.mtime - a.mtime);
return conversationFiles.length > 0 ? conversationFiles[0].path : '';
} catch (error) {
return '';
}
}
private generateLookupKeys(content: string): string[] {
const keys = [];
// Extract key terms from message content
const terms = content.toLowerCase().split(/\s+/);
const importantTerms = terms.filter(term =>
term.length > 3 &&
!['this', 'that', 'with', 'from', 'have', 'been', 'will'].includes(term)
);
keys.push(...importantTerms.slice(0, 5));
// Add context-specific keys
keys.push('current-task', 'active-files', 'recent-errors', 'collaboration');
return keys;
}
private extractNeedsHelp(): string[] {
try {
const recentContext = this.extractRecentJSONLContext();
const helpIndicators = [];
for (const message of recentContext.slice(-5)) {
const content = this.extractContentFromMessage(message);
if (content && (content.includes('error') || content.includes('failed') || content.includes('issue'))) {
helpIndicators.push(content.slice(0, 80));
}
}
return helpIndicators;
} catch (error) {
return [];
}
}
private extractCanShare(): string[] {
try {
const context = this.generateContextSummary();
const shared = [];
if (context.activeFiles.length > 0) {
shared.push(`Working with: ${context.activeFiles.join(', ')}`);
}
if (context.currentTask && context.currentTask !== 'Active') {
shared.push(`Current task: ${context.currentTask}`);
}
return shared;
} catch (error) {
return [];
}
}
private extractWarnings(): string[] {
try {
const recentContext = this.extractRecentJSONLContext();
const warnings = [];
for (const message of recentContext.slice(-3)) {
const content = this.extractContentFromMessage(message);
if (content && content.includes('warning')) {
warnings.push(content.slice(0, 100));
}
}
return warnings;
} catch (error) {
return [];
}
}
// Gather raw Claude knowledge (under the hood)
gatherRawClaudeKnowledge(): {
instances: any[];
rawActivities: any[];
messageBus: InterClaudeMessage[];
crossProjectData: any;
} {
try {
const instances = this.discoverInstances();
const rawActivities = instances.map(instance => ({
pid: instance.pid,
activity: this.parseLiveActivity(instance.pid),
instanceData: instance
}));
const crossProjectData = {
allFiles: [...new Set(rawActivities.flatMap(a => a.activity.files || []))],
allProjects: [...new Set(instances.map(i => i.projectPath?.split('/').pop()).filter(Boolean))],
totalInstances: instances.length
};
return {
instances,
rawActivities,
messageBus: this.messageMailbox,
crossProjectData
};
} catch (error) {
console.error('[Claude Senator] Error gathering raw knowledge:', error);
return { instances: [], rawActivities: [], messageBus: [], crossProjectData: {} };
}
}
// Process raw knowledge into human-readable format
formatForHuman(rawKnowledge: any): {
summary: string;
currentWork: string[];
importantUpdates: string[];
overallStatus: string;
} {
try {
const { rawActivities, messageBus, crossProjectData } = rawKnowledge;
// Extract human-readable current work
const currentWork = rawActivities
.filter((a: any) => a.activity.task !== 'Active' && a.activity.task !== 'Recent activity')
.map((a: any) => {
const project = a.activity.workingDir?.split('/').pop() || 'unknown';
return `${project}: ${a.activity.task}`;
})
.slice(0, 5);
// Extract important updates (special messages)
const importantUpdates = messageBus
.filter((msg: any) => msg.type === 'shared_status' || msg.options?.urgent)
.map((msg: any) => (msg.content || '').replace('📬 Shared: ', ''))
.slice(0, 3);
// Generate summary
const activeProjects = crossProjectData.allProjects?.length || 0;
const activeInstances = crossProjectData.totalInstances || 0;
const summary = activeInstances > 1
? `${activeInstances} Claude instances active across ${activeProjects} project${activeProjects !== 1 ? 's' : ''}`
: 'Single Claude instance active';
const overallStatus = currentWork.length > 0
? 'Multiple tasks in progress'
: 'Ready for work';
return {
summary,
currentWork,
importantUpdates,
overallStatus
};
} catch (error) {
console.error('[Claude Senator] Error formatting for human:', error);
return {
summary: 'Error processing status',
currentWork: [],
importantUpdates: [],
overallStatus: 'Unknown'
};
}
}
// Ultra-dense Claude messaging + Beautiful human updates
async sendMessage(targetPid: number, humanMessage: string): Promise<{success: boolean, context: string}> {
try {
// Ensure directory exists
this.ensureDirectories();
const messageData = this.createMessage(humanMessage);
// Get reliable Claude PID (prefer parent Claude process over MCP server process)
const claudePid = process.ppid || process.pid;
const currentProject = process.cwd().split('/').pop();
// Create proper InterClaudeMessage format with ultra-dense context
const message: InterClaudeMessage = {
id: randomUUID(),
from: claudePid,
to: targetPid === -1 ? 'all' : targetPid,
type: 'ultra_dense_message',
content: humanMessage,
timestamp: Date.now(),
options: {
claudeContext: messageData, // Rich data for Claude reasoning
senderProject: currentProject, // Human-readable project context
senderWorkingDir: process.cwd() // Full path for Claude
}
};
const messageFile = join(this.baseDir, `${claudePid}-${Date.now()}.json`);
writeFileSync(messageFile, JSON.stringify(message));
// Extract context description for human
const context = this.summarizeContextForHuman(messageData);
return { success: true, context };
} catch (error) {
console.error('[Claude Senator] sendMessage failed:', error);
throw error; // Don't hide the real error - let the human see what's wrong
}
}
private createMessage(humanMessage: string): any {
// SMART POINTER CONTEXT - Reference existing ~/.claude data, create nothing new
const workingDir = process.cwd();
const claudeDir = join(homedir(), '.claude');
const projectEncoded = workingDir.replace(/\//g, '-').replace(/^-/, '');
const pid = process.ppid || process.pid;
return {
h: humanMessage, // Human request
pid: pid, // Claude PID
ts: Date.now(), // Timestamp
cwd: workingDir, // Current working directory
// SMART POINTERS TO EXISTING ~/.claude DATA (read-only references)
ctx_pointers: {
projects_dir: `~/.claude/projects/${projectEncoded}`, // Project data if exists
conversation_history: this.getConversationPointer(), // Recent conversation if available
git_context: this.getGitContextString(), // Compact git state
active_files: this.getActiveFilesArray(), // Current file positions
current_task: this.getCurrentTaskFromContext(), // What I'm working on
collaboration_intent: this.determineCollaborationIntent(humanMessage) // Why reaching out
}
};
}
private summarizeContextForHuman(messageData: any): string {
const project = messageData.cwd?.split('/').pop() || 'unknown';
const task = messageData.ctx_pointers?.current_task || 'working';
const intent = messageData.ctx_pointers?.collaboration_intent || 'collaboration';
return `${task} • ${intent} • project "${project}"`;
}
// SMART POINTER HELPER METHODS (read-only, no file creation)
private getConversationPointer(): string | null {
try {
const claudeDir = join(homedir(), '.claude');
const projectEncoded = process.cwd().replace(/\//g, '-').replace(/^-/, '');
const projectDir = join(claudeDir, 'projects', projectEncoded);
if (existsSync(projectDir)) {
// Find most recent conversation file if it exists
const files = readdirSync(projectDir).filter(f => f.includes('conversation') || f.includes('session'));
if (files.length > 0) {
return `~/.claude/projects/${projectEncoded}/${files[files.length - 1]}`;
}
}
return null;
} catch (error) {
return null;
}
}
private getGitContextString(): string {
try {
const { execSync } = require('child_process');
const branch = execSync('git branch --show-current 2>/dev/null', { encoding: 'utf8' }).trim();
const commit = execSync('git rev-parse --short HEAD 2>/dev/null', { encoding: 'utf8' }).trim();
const status = execSync('git status --porcelain 2>/dev/null', { encoding: 'utf8' }).trim();
const dirty = status ? '+dirty' : '';
return `${branch}@${commit}${dirty}`;
} catch (error) {
return 'no-git';
}
}
private getActiveFilesArray(): string[] {
try {
// Try to detect active files from recent git changes or common patterns
const { execSync } = require('child_process');
const recentFiles = execSync('git diff --name-only HEAD~1 2>/dev/null', { encoding: 'utf8' }).trim().split('\n').filter((f: string) => f);
return recentFiles.slice(0, 5); // Max 5 recent files
} catch (error) {
return [];
}
}
private getCurrentTaskFromContext(): string {
try {
// Extract task from current directory context
const workingDir = process.cwd();
const projectName = workingDir.split('/').pop() || 'unknown';
const activity = this.parseLiveActivity(process.ppid || process.pid);
return activity.task || `Working in ${projectName}`;
} catch (error) {
return 'Active development';
}
}
private determineCollaborationIntent(humanMessage: string): string {
const msg = humanMessage.toLowerCase();
if (msg.includes('help') || msg.includes('debug') || msg.includes('fix')) return 'debugging_assistance';
if (msg.includes('review') || msg.includes('check') || msg.includes('look')) return 'code_review';
if (msg.includes('test') || msg.includes('status') || msg.includes('communication')) return 'testing';
if (msg.includes('handoff') || msg.includes('take over') || msg.includes('continue')) return 'task_handoff';
return 'general_collaboration';
}
// CONTEXT RECONSTRUCTION - Follow smart pointers to rebuild full context
private reconstructRichContext(message: any): any {
try {
const ctx = message.options?.claudeContext;
if (!ctx?.ctx_pointers) return null;
const pointers = ctx.ctx_pointers;
const reconstructed: any = {
sender_info: {
human_message: ctx.h,
pid: ctx.pid,
working_directory: ctx.cwd,
timestamp: ctx.ts
},
collaboration_context: {
current_task: pointers.current_task,
intent: pointers.collaboration_intent,
git_state: pointers.git_context,
active_files: pointers.active_files
},
shared_data_access: {
projects_dir: pointers.projects_dir,
conversation_history: pointers.conversation_history,
// Additional context can be loaded on-demand via these pointers
}
};
// Optionally load conversation history if pointer exists
if (pointers.conversation_history) {
reconstructed.conversation_snippet = this.loadConversationSnippet(pointers.conversation_history);
}
return reconstructed;
} catch (error) {
console.error('[Claude Senator] Error reconstructing context:', error);
return null;
}
}
private loadConversationSnippet(conversationPointer: string): string | null {
try {
// Convert ~/ to actual path
const actualPath = conversationPointer.replace('~/', join(homedir(), '/'));
if (existsSync(actualPath)) {
const content = readFileSync(actualPath, 'utf8');
// Return last few lines for context
const lines = content.split('\n');
return lines.slice(-10).join('\n');
}
return null;
} catch (error) {
return null;
}
}
autoProcessInbox(): {processedCount: number, responses: number} {
try {
const messages = this.getIncomingMessages();
let responses = 0;
messages.forEach(msg => {
if (this.shouldAutoRespond(msg)) {
this.sendAutoResponse(msg);
responses++;
}
this.markMessageAsRead(msg);
});
return { processedCount: messages.length, responses };
} catch (error) {
return { processedCount: 0, responses: 0 };
}
}
autoReadAndRespond(): {messages: Array<{from: string, summary: string, response: string, context: string, rich_context?: any}>} {
try {
const messages = this.getIncomingMessages();
const processed = messages.map(msg => ({
from: `Claude ${msg.from}`,
summary: this.extractHumanSummary(msg),
context: this.summarizeContextLearned(msg),
response: this.generateIntelligentResponse(msg),
rich_context: this.reconstructRichContext(msg) // NEW: Full context via pointers
}));
// Cleanup read messages
messages.forEach(msg => this.markMessageAsRead(msg));
return { messages: processed };
} catch (error) {
return { messages: [] };
}
}
getDetailedStatus(): {local: string, others: string[], pending: number} {
try {
const instances = this.discoverInstances();
const messages = this.getIncomingMessages();
const currentTask = this.extractCurrentTask();
const others = instances
.filter(i => i.pid !== (process.ppid || process.pid))
.map(i => `Claude ${i.pid} ${i.currentTask || 'active'}`);
return {
local: currentTask,
others: others,
pending: messages.length
};
} catch (error) {
return { local: 'unknown', others: [], pending: 0 };
}
}
// MAXIMUM VALUE CONTEXT EXTRACTORS - Rich collaboration intelligence
private findSessionData(claudeDir: string, projectEncoded: string): any {
try {
const projectPath = join(claudeDir, 'projects', projectEncoded);
if (!existsSync(projectPath)) return { available: false };
const files = readdirSync(projectPath).filter(f => f.endsWith('.jsonl'));
const latest = files.length > 0 ? files[files.length - 1] : null;
return {
available: true,
path: projectPath,
latest: latest,
count: files.length
};
} catch {
return { available: false };
}
}
private getCurrentTaskContext(): any {
// Extract what I'm actually working on from recent activity
try {
const cwd = process.cwd();
const recentFiles = this.getModifiedFiles();
const task = this.inferCurrentTask(cwd, recentFiles);
return {
task,
files: recentFiles.slice(0, 5),
focus: this.getCodeFocus(recentFiles)
};
} catch {
return { task: 'unknown', files: [], focus: null };
}
}
private analyzeProjectStack(): any {
const cwd = process.cwd();
const stack: any = { type: 'unknown', tools: [], langs: [] };
try {
if (existsSync(join(cwd, 'package.json'))) {
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
stack.type = 'node';
stack.tools = ['npm'];
stack.deps = Object.keys(pkg.dependencies || {}).slice(0, 5);
stack.langs = ['javascript', 'typescript'];
}
if (existsSync(join(cwd, 'Cargo.toml'))) {
stack.type = 'rust';
stack.tools = ['cargo'];
stack.langs = ['rust'];
}
if (existsSync(join(cwd, 'requirements.txt'))) {
stack.type = 'python';
stack.tools = ['pip'];
stack.langs = ['python'];
}
return stack;
} catch {
return stack;
}
}
private getWorkingState(): any {
try {
return {
modified: this.getModifiedFiles().length,
branch: this.getCurrentBranch(),
dirty: this.hasUncommittedChanges()
};
} catch {
return { modified: 0, branch: null, dirty: false };
}
}
private getMyExpertise(): string[] {
const cwd = process.cwd();
const expertise = ['file-ops', 'analysis'];
try {
if (existsSync(join(cwd, 'src'))) expertise.push('src-code');
if (existsSync(join(cwd, 'test'))) expertise.push('testing');
if (existsSync(join(cwd, 'package.json'))) expertise.push('node-js');
if (existsSync(join(cwd, '.git'))) expertise.push('git');
} catch {}
return expertise;
}
private getBlockingIssues(): string[] {
const issues = [];
try {
const cwd = process.cwd();
if (!existsSync(join(cwd, 'node_modules')) && existsSync(join(cwd, 'package.json'))) {
issues.push('deps-not-installed');
}
} catch {}
return issues;
}
private getSharedMemoryPointers(): any {
try {
return {
tmp: this.baseDir,
claude: join(homedir(), '.claude'),
project: process.cwd()
};
} catch {
return {};
}
}
// Helper methods
private getModifiedFiles(): string[] {
try {
const files = readdirSync(process.cwd(), { recursive: true })
.filter((f: any) => typeof f === 'string' && !f.includes('node_modules') && !f.startsWith('.'))
.slice(0, 10);
return files as string[];
} catch {
return [];
}
}
private inferCurrentTask(cwd: string, files: string[]): string {
if (files.some(f => f.includes('test'))) return 'testing';
if (files.some(f => f.includes('src'))) return 'development';
if (files.some(f => f.includes('.md'))) return 'documentation';
if (cwd.includes('claude-senator')) return 'inter-claude-communication';
return 'file-work';
}
private getCodeFocus(files: string[]): string | null {
const srcFiles = files.filter(f => f.includes('src/'));
if (srcFiles.length > 0) return srcFiles[0];
return null;
}
private getCurrentBranch(): string | null {
try {
const gitHead = join(process.cwd(), '.git', 'HEAD');
if (existsSync(gitHead)) {
const head = readFileSync(gitHead, 'utf8').trim();
if (head.startsWith('ref: refs/heads/')) {
return head.replace('ref: refs/heads/', '');
}
}
} catch {}
return null;
}
private hasUncommittedChanges(): boolean {
try {
const result = execSync('git status --porcelain', { cwd: process.cwd(), encoding: 'utf8' });
return result.trim().length > 0;
} catch {
return false;
}
}
private shouldAutoRespond(msg: any): boolean {
return msg.h && msg.h.includes('help');
}
private extractHumanSummary(msg: any): string {
return msg.content || msg.h || 'No content';
}
private summarizeContextLearned(msg: any): string {
if (!msg.options?.claudeContext) return 'No context data';
const ctx = msg.options.claudeContext;
const insights = [];
if (ctx.context?.task) insights.push(`Working on: ${ctx.context.task}`);
if (ctx.stack?.type) insights.push(`Stack: ${ctx.stack.type}`);
if (ctx.expertise?.length) insights.push(`Skills: ${ctx.expertise.slice(0, 3).join(', ')}`);
if (ctx.session?.available) insights.push(`Session data: ${ctx.session.count} files`);
if (ctx.blocking?.length) insights.push(`Blocked by: ${ctx.blocking.join(', ')}`);
return insights.length > 0 ? insights.join(' • ') : 'Basic context only';
}
private generateIntelligentResponse(msg: any): string {
if (!msg.options?.claudeContext) return 'Acknowledged';
const ctx = msg.options.claudeContext;
const request = msg.content || msg.h;
// Generate intelligent response based on rich context
if (request.includes('broadcast') && ctx.expertise?.includes('src-code')) {
return 'Can help with broadcast implementation - I have src-code expertise and can analyze your messaging architecture';
}
if (request.includes('help') && ctx.context?.task === 'inter-claude-communication') {
return `Understanding your ${ctx.stack?.type || 'project'} work - ready to collaborate on inter-Claude features`;
}
if (ctx.blocking?.length > 0) {
return `Noted your blocking issues: ${ctx.blocking.join(', ')} - can assist with resolution`;
}
return `Ready to collaborate on your ${ctx.context?.task || 'project'} work`;
}
private sendAutoResponse(msg: any): void {
// Simple auto-response for now
}
private markMessageAsRead(msg: any): void {
if (msg._filePath && existsSync(msg._filePath)) {
try {
unlinkSync(msg._filePath);
} catch (error) {
// Ignore cleanup errors
}
}
}
private extractCurrentTask(): string {
const context = this.generateContextSummary();
return context.currentTask || 'implementing async message broker';
}
async broadcastMessage(message: Omit<InterClaudeMessage, 'to'>): Promise<number> {
const { success } = await this.sendMessage(-1, message.content);
return success ? 1 : 0;
}
discoverInstances(): ClaudeInstance[] {
const now = Date.now();
if (this.discoveryCache && (now - this.discoveryCache.timestamp) < 5000) {
return this.discoveryCache.instances;
}
const instances: ClaudeInstance[] = [];
try {
// Ensure directories exist before discovery
this.ensureDirectories();
if (!existsSync(this.baseDir)) {
this.discoveryCache = { instances, timestamp: now };
return instances;
}
const instanceFiles = readdirSync(this.baseDir).filter(f => f.startsWith('claude-') && f.endsWith('.json'));
const activePids = this.getActivePids();
for (const file of instanceFiles) {
const pidMatch = file.match(/claude-(\d+)\.json/);
if (!pidMatch) continue;
const pid = parseInt(pidMatch[1]);
if (!activePids.includes(pid)) continue;
try {
const filePath = join(this.baseDir, file);
const data = readFileSync(filePath, 'utf8');
const instance = JSON.parse(data);
instances.push(instance);
} catch (error) {
console.error(`[Claude Senator] Failed to read instance file ${file}:`, error);
}
}
console.error(`[Claude Senator] Discovered ${instances.length} active Claude instances`);
this.discoveryCache = { instances, timestamp: now };
} catch (error) {
console.error('[Claude Senator] Failed to discover instances:', error);
this.discoveryCache = { instances, timestamp: now };
}
return instances;
}
private updateInstanceStatus(pid: number, status: string): void {
// This could be enhanced to track status of other instances
console.error(`[Claude Senator] Instance ${pid} status: ${status}`);
}
updateStatus(status: string, currentTask?: string): void {
this.sessionData.status = status;
this.sessionData.currentTask = currentTask;
this.sessionData.lastSeen = Date.now();
this.registerSession();
}
getSessionData(): ClaudeInstance {
return { ...this.sessionData };
}
private cleanup(): void {
try {
// Close file watcher
if (this.messageWatcher) {
this.messageWatcher.close();
this.messageWatcher = null;
}
// Clean up our instance file when exiting
if (existsSync(this.instanceFile)) {
unlinkSync(this.instanceFile);
console.error(`[Claude Senator] Cleaned up instance file: ${this.instanceFile}`);
}
} catch (error) {
// Ignore cleanup errors
}
}
}