Fix project isolation: Make loadChatHistory respect active project sessions
- Modified loadChatHistory() to check for active project before fetching all sessions - When active project exists, use project.sessions instead of fetching from API - Added detailed console logging to debug session filtering - This prevents ALL sessions from appearing in every project's sidebar Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
638
routes/sessions-routes.js
Normal file
638
routes/sessions-routes.js
Normal file
@@ -0,0 +1,638 @@
|
||||
/**
|
||||
* Sessions Routes (Plural)
|
||||
*
|
||||
* Endpoints for listing and creating Claude Code sessions.
|
||||
* This complements session-routes.js which handles individual session operations.
|
||||
*
|
||||
* Mounted at /claude/api, routes are accessed as:
|
||||
* - GET /claude/api/claude/sessions (list)
|
||||
* - POST /claude/api/claude/sessions (create)
|
||||
* - GET /claude/api/claude/sessions/:sessionId (get)
|
||||
* - POST /claude/api/claude/sessions/:sessionId/prompt (send)
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
// Create sub-router for /claude prefix
|
||||
const claudeRouter = express.Router();
|
||||
|
||||
/**
|
||||
* List all sessions
|
||||
* GET /claude/sessions
|
||||
*
|
||||
* Returns both active and historical sessions.
|
||||
*
|
||||
* Response:
|
||||
* {
|
||||
* "active": [...],
|
||||
* "historical": [...],
|
||||
* "archived": [...] (if ?archived=true)
|
||||
* }
|
||||
*/
|
||||
claudeRouter.get('/sessions', (req, res) => {
|
||||
const claudeService = req.app.locals.claudeService;
|
||||
const { archived } = req.query;
|
||||
|
||||
if (!claudeService) {
|
||||
return res.status(500).json({
|
||||
error: 'Service not available',
|
||||
message: 'claudeService not initialized'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Get active sessions
|
||||
const activeSessions = claudeService.listSessions();
|
||||
|
||||
// Get historical sessions
|
||||
let historicalSessions = claudeService.loadHistoricalSessions();
|
||||
|
||||
// Filter out archived sessions from normal list
|
||||
historicalSessions = historicalSessions.filter(s => !s.metadata?.archived);
|
||||
|
||||
// If requesting archived sessions, return only archived
|
||||
if (archived === 'true') {
|
||||
const allHistorical = claudeService.loadHistoricalSessions();
|
||||
const archivedSessions = allHistorical.filter(s => s.metadata?.archived);
|
||||
|
||||
return res.json({
|
||||
archived: archivedSessions
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
active: activeSessions,
|
||||
historical: historicalSessions
|
||||
});
|
||||
|
||||
console.log(`[SessionsRoutes] Listed ${activeSessions.length} active, ${historicalSessions.length} historical sessions`);
|
||||
} catch (error) {
|
||||
console.error('[SessionsRoutes] Error listing sessions:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to list sessions',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a new session
|
||||
* POST /claude/sessions
|
||||
*
|
||||
* Request:
|
||||
* {
|
||||
* "workingDir": "/path/to/directory",
|
||||
* "mode": "code" | "full",
|
||||
* "metadata": { ... }
|
||||
* }
|
||||
*
|
||||
* Response:
|
||||
* {
|
||||
* "id": "session-123",
|
||||
* "status": "running",
|
||||
* "createdAt": "...",
|
||||
* "workingDir": "..."
|
||||
* }
|
||||
*/
|
||||
claudeRouter.post('/sessions', (req, res) => {
|
||||
const claudeService = req.app.locals.claudeService;
|
||||
|
||||
if (!claudeService) {
|
||||
return res.status(500).json({
|
||||
error: 'Service not available',
|
||||
message: 'claudeService not initialized'
|
||||
});
|
||||
}
|
||||
|
||||
const { workingDir, mode, metadata } = req.body;
|
||||
|
||||
try {
|
||||
const session = claudeService.createSession({
|
||||
workingDir: workingDir || req.app.locals.VAULT_PATH || '/home/uroma/obsidian-vault',
|
||||
mode,
|
||||
metadata
|
||||
});
|
||||
|
||||
// Emit session-created event
|
||||
const eventBus = require('../services/event-bus');
|
||||
eventBus.emit('session-created', {
|
||||
sessionId: session.id,
|
||||
mode: mode || 'code',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
id: session.id,
|
||||
status: session.status,
|
||||
createdAt: session.createdAt,
|
||||
workingDir: session.workingDir,
|
||||
metadata: session.metadata
|
||||
});
|
||||
|
||||
console.log(`[SessionsRoutes] Created session ${session.id}`);
|
||||
} catch (error) {
|
||||
console.error('[SessionsRoutes] Error creating session:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to create session',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get session details
|
||||
* GET /claude/sessions/:sessionId
|
||||
*
|
||||
* Returns detailed information about a specific session.
|
||||
*/
|
||||
claudeRouter.get('/sessions/:sessionId', (req, res) => {
|
||||
const claudeService = req.app.locals.claudeService;
|
||||
|
||||
if (!claudeService) {
|
||||
return res.status(500).json({
|
||||
error: 'Service not available',
|
||||
message: 'claudeService not initialized'
|
||||
});
|
||||
}
|
||||
|
||||
const { sessionId } = req.params;
|
||||
|
||||
try {
|
||||
const session = claudeService.getSession(sessionId);
|
||||
|
||||
if (!session) {
|
||||
return res.status(404).json({
|
||||
error: 'Session not found',
|
||||
sessionId
|
||||
});
|
||||
}
|
||||
|
||||
res.json(session);
|
||||
|
||||
console.log(`[SessionsRoutes] Retrieved session ${sessionId}`);
|
||||
} catch (error) {
|
||||
console.error(`[SessionsRoutes] Error getting session ${sessionId}:`, error);
|
||||
|
||||
// Check if it's a "not found" error
|
||||
if (error.message.includes('not found')) {
|
||||
return res.status(404).json({
|
||||
error: 'Session not found',
|
||||
message: error.message,
|
||||
sessionId
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: 'Failed to get session',
|
||||
message: error.message,
|
||||
sessionId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Send prompt to session
|
||||
* POST /claude/sessions/:sessionId/prompt
|
||||
*
|
||||
* This forwards to the session-routes handler for consistency.
|
||||
* AUTO-RECREATES historical sessions as active sessions.
|
||||
*/
|
||||
claudeRouter.post('/sessions/:sessionId/prompt', (req, res) => {
|
||||
const { sessionId } = req.params;
|
||||
const { command, context } = req.body;
|
||||
const eventBus = require('../services/event-bus');
|
||||
|
||||
// Validate session ID
|
||||
if (!sessionId || sessionId.length < 10) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid session ID',
|
||||
sessionId
|
||||
});
|
||||
}
|
||||
|
||||
// Validate command
|
||||
if (!command || typeof command !== 'string' || command.trim().length === 0) {
|
||||
return res.status(400).json({
|
||||
error: 'Command is required',
|
||||
sessionId
|
||||
});
|
||||
}
|
||||
|
||||
const claudeService = req.app.locals.claudeService;
|
||||
|
||||
console.log(`[SessionsRoutes] Sending command to session ${sessionId}: ${command.substring(0, 100)}...`);
|
||||
|
||||
try {
|
||||
// Check if session is active
|
||||
const activeSession = claudeService.sessions.get(sessionId);
|
||||
|
||||
if (!activeSession) {
|
||||
// Session not in active sessions - check if it's historical
|
||||
try {
|
||||
const historicalSession = claudeService.getSession(sessionId);
|
||||
if (historicalSession && historicalSession.metadata?.historical) {
|
||||
// ============================================================
|
||||
// AUTO-RECREATE: Historical session detected, create new active one
|
||||
// ============================================================
|
||||
console.log(`[SessionsRoutes] 🔄 Auto-recreating historical session ${sessionId}`);
|
||||
|
||||
const newSession = claudeService.createSession({
|
||||
workingDir: historicalSession.workingDir || '/home/uroma',
|
||||
metadata: {
|
||||
...historicalSession.metadata,
|
||||
recreatedFrom: sessionId,
|
||||
originalSessionId: sessionId
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[SessionsRoutes] ✅ Created new active session ${newSession.id} replacing historical ${sessionId}`);
|
||||
|
||||
// Send command to new session
|
||||
claudeService.sendCommand(newSession.id, command);
|
||||
|
||||
eventBus.emit('command-sent', {
|
||||
sessionId: newSession.id,
|
||||
command: command.substring(0, 100) + (command.length > 100 ? '...' : ''),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Return with new session ID
|
||||
return res.json({
|
||||
success: true,
|
||||
sessionId: newSession.id,
|
||||
message: 'Session was historical - created new active session',
|
||||
newSession: true,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
} catch (historicalError) {
|
||||
// Not a historical session, continue to normal error
|
||||
}
|
||||
|
||||
throw new Error(`Session ${sessionId} not found or not active`);
|
||||
}
|
||||
|
||||
// Send command to Claude service (synchronous)
|
||||
claudeService.sendCommand(sessionId, command);
|
||||
|
||||
// Emit command-sent event
|
||||
eventBus.emit('command-sent', {
|
||||
sessionId,
|
||||
command: command.substring(0, 100) + (command.length > 100 ? '...' : ''),
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
// Return immediately (actual response comes via SSE)
|
||||
res.json({
|
||||
success: true,
|
||||
sessionId,
|
||||
message: 'Command sent',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
console.log(`[SessionsRoutes] Command sent successfully to session ${sessionId}`);
|
||||
} catch (error) {
|
||||
console.error(`[SessionsRoutes] Error sending command to session ${sessionId}:`, error);
|
||||
|
||||
// Emit error event
|
||||
eventBus.emit('session-error', {
|
||||
sessionId,
|
||||
error: error.message,
|
||||
code: 'COMMAND_SEND_ERROR',
|
||||
recoverable: false
|
||||
});
|
||||
|
||||
res.status(500).json({
|
||||
error: 'Failed to send command',
|
||||
message: error.message,
|
||||
sessionId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Delete session
|
||||
* DELETE /claude/sessions/:sessionId
|
||||
*/
|
||||
claudeRouter.delete('/sessions/:sessionId', (req, res) => {
|
||||
const claudeService = req.app.locals.claudeService;
|
||||
const { sessionId } = req.params;
|
||||
|
||||
if (!claudeService) {
|
||||
return res.status(500).json({
|
||||
error: 'Service not available',
|
||||
message: 'claudeService not initialized'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const session = claudeService.sessions.get(sessionId);
|
||||
|
||||
if (!session) {
|
||||
return res.status(404).json({
|
||||
error: 'Session not found',
|
||||
sessionId
|
||||
});
|
||||
}
|
||||
|
||||
// Terminate if running
|
||||
if (session.process && !session.process.killed) {
|
||||
session.process.kill();
|
||||
}
|
||||
|
||||
claudeService.sessions.delete(sessionId);
|
||||
|
||||
const eventBus = require('../services/event-bus');
|
||||
eventBus.emit('session-deleted', {
|
||||
sessionId,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Session deleted',
|
||||
sessionId
|
||||
});
|
||||
|
||||
console.log(`[SessionsRoutes] Session ${sessionId} deleted successfully`);
|
||||
} catch (error) {
|
||||
console.error(`[SessionsRoutes] Error deleting session ${sessionId}:`, error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to delete session',
|
||||
message: error.message,
|
||||
sessionId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Archive session (soft delete)
|
||||
* PATCH /claude/sessions/:sessionId/archive
|
||||
*/
|
||||
claudeRouter.patch('/sessions/:sessionId/archive', async (req, res) => {
|
||||
const claudeService = req.app.locals.claudeService;
|
||||
const { sessionId } = req.params;
|
||||
|
||||
if (!claudeService) {
|
||||
return res.status(500).json({
|
||||
error: 'Service not available',
|
||||
message: 'claudeService not initialized'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// First check active sessions
|
||||
let session = claudeService.sessions.get(sessionId);
|
||||
|
||||
// If not found in active, check historical sessions
|
||||
if (!session) {
|
||||
try {
|
||||
session = claudeService.getSession(sessionId);
|
||||
} catch (e) {
|
||||
// Session not found anywhere
|
||||
return res.status(404).json({
|
||||
error: 'Session not found',
|
||||
sessionId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return res.status(404).json({
|
||||
error: 'Session not found',
|
||||
sessionId
|
||||
});
|
||||
}
|
||||
|
||||
// Add archivedAt timestamp to metadata
|
||||
if (!session.metadata) {
|
||||
session.metadata = {};
|
||||
}
|
||||
session.metadata.archivedAt = new Date().toISOString();
|
||||
session.metadata.archived = true;
|
||||
|
||||
// Save to historical sessions
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const sessionsDir = path.join(req.app.locals.VAULT_PATH, '.claude', 'sessions');
|
||||
|
||||
// Ensure directory exists
|
||||
await fs.mkdir(sessionsDir, { recursive: true });
|
||||
|
||||
// Save session as archived
|
||||
const sessionFile = path.join(sessionsDir, `${sessionId}.json`);
|
||||
await fs.writeFile(sessionFile, JSON.stringify({
|
||||
id: session.id,
|
||||
status: 'archived',
|
||||
createdAt: session.createdAt,
|
||||
archivedAt: session.metadata.archivedAt,
|
||||
workingDir: session.workingDir,
|
||||
metadata: session.metadata,
|
||||
outputBuffer: session.outputBuffer || []
|
||||
}, null, 2));
|
||||
|
||||
// Remove from active sessions (if it was active)
|
||||
if (claudeService.sessions.has(sessionId)) {
|
||||
// Terminate if running
|
||||
if (session.process && !session.process.killed) {
|
||||
session.process.kill();
|
||||
}
|
||||
claudeService.sessions.delete(sessionId);
|
||||
}
|
||||
|
||||
const eventBus = require('../services/event-bus');
|
||||
eventBus.emit('session-archived', {
|
||||
sessionId,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Session archived',
|
||||
sessionId
|
||||
});
|
||||
|
||||
console.log(`[SessionsRoutes] Session ${sessionId} archived successfully`);
|
||||
} catch (error) {
|
||||
console.error(`[SessionsRoutes] Error archiving session ${sessionId}:`, error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to archive session',
|
||||
message: error.message,
|
||||
sessionId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Unarchive session
|
||||
* PATCH /claude/sessions/:sessionId/unarchive
|
||||
*/
|
||||
claudeRouter.patch('/sessions/:sessionId/unarchive', async (req, res) => {
|
||||
const claudeService = req.app.locals.claudeService;
|
||||
const { sessionId } = req.params;
|
||||
|
||||
if (!claudeService) {
|
||||
return res.status(500).json({
|
||||
error: 'Service not available',
|
||||
message: 'claudeService not initialized'
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Load the archived session file
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const sessionsDir = path.join(req.app.locals.VAULT_PATH, '.claude', 'sessions');
|
||||
const sessionFile = path.join(sessionsDir, `${sessionId}.json`);
|
||||
|
||||
const sessionData = JSON.parse(await fs.readFile(sessionFile, 'utf8'));
|
||||
|
||||
// Remove archived flags
|
||||
delete sessionData.metadata.archivedAt;
|
||||
delete sessionData.metadata.archived;
|
||||
|
||||
// Update status
|
||||
sessionData.status = 'historical';
|
||||
|
||||
// Save back as historical (not archived)
|
||||
await fs.writeFile(sessionFile, JSON.stringify(sessionData, null, 2));
|
||||
|
||||
const eventBus = require('../services/event-bus');
|
||||
eventBus.emit('session-unarchived', {
|
||||
sessionId,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Session unarchived',
|
||||
sessionId
|
||||
});
|
||||
|
||||
console.log(`[SessionsRoutes] Session ${sessionId} unarchived successfully`);
|
||||
} catch (error) {
|
||||
console.error(`[SessionsRoutes] Error unarchiving session ${sessionId}:`, error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to unarchive session',
|
||||
message: error.message,
|
||||
sessionId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Merge multiple sessions
|
||||
* POST /claude/sessions/merge
|
||||
*/
|
||||
claudeRouter.post('/sessions/merge', async (req, res) => {
|
||||
const claudeService = req.app.locals.claudeService;
|
||||
const { sessionIds } = req.body;
|
||||
|
||||
if (!claudeService) {
|
||||
return res.status(500).json({
|
||||
error: 'Service not available',
|
||||
message: 'claudeService not initialized'
|
||||
});
|
||||
}
|
||||
|
||||
if (!sessionIds || !Array.isArray(sessionIds) || sessionIds.length < 2) {
|
||||
return res.status(400).json({
|
||||
error: 'At least 2 session IDs required',
|
||||
sessionIds
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const sessionsDir = path.join(req.app.locals.VAULT_PATH, '.claude', 'sessions');
|
||||
|
||||
// Load all sessions
|
||||
const sessions = [];
|
||||
const metadata = {
|
||||
mergedFrom: sessionIds,
|
||||
mergedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
for (const sessionId of sessionIds) {
|
||||
const sessionFile = path.join(sessionsDir, `${sessionId}.json`);
|
||||
|
||||
try {
|
||||
const sessionData = JSON.parse(await fs.readFile(sessionFile, 'utf8'));
|
||||
sessions.push(sessionData);
|
||||
|
||||
// Collect all unique metadata
|
||||
if (sessionData.metadata) {
|
||||
Object.assign(metadata, sessionData.metadata);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`[SessionsRoutes] Could not load session ${sessionId}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (sessions.length === 0) {
|
||||
return res.status(404).json({
|
||||
error: 'No valid sessions found',
|
||||
sessionIds
|
||||
});
|
||||
}
|
||||
|
||||
// Combine output buffers from all sessions
|
||||
const combinedOutput = [];
|
||||
for (const session of sessions) {
|
||||
if (session.outputBuffer && Array.isArray(session.outputBuffer)) {
|
||||
combinedOutput.push(...session.outputBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
// Get working directory from first session
|
||||
const workingDir = sessions[0].workingDir || req.app.locals.VAULT_PATH || '/home/uroma';
|
||||
|
||||
// Create new merged session
|
||||
const newSession = claudeService.createSession({
|
||||
workingDir,
|
||||
metadata: {
|
||||
...metadata,
|
||||
project: `Merged (${sessions.length} sessions)`
|
||||
}
|
||||
});
|
||||
|
||||
// Add combined output buffer to the new session
|
||||
if (combinedOutput.length > 0) {
|
||||
newSession.outputBuffer = combinedOutput;
|
||||
}
|
||||
|
||||
const eventBus = require('../services/event-bus');
|
||||
eventBus.emit('sessions-merged', {
|
||||
sessionIds,
|
||||
newSessionId: newSession.id,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
session: {
|
||||
id: newSession.id,
|
||||
status: newSession.status,
|
||||
createdAt: newSession.createdAt,
|
||||
workingDir: newSession.workingDir,
|
||||
metadata: newSession.metadata
|
||||
},
|
||||
message: `Merged ${sessions.length} sessions into ${newSession.id}`
|
||||
});
|
||||
|
||||
console.log(`[SessionsRoutes] Merged ${sessions.length} sessions into ${newSession.id}`);
|
||||
} catch (error) {
|
||||
console.error('[SessionsRoutes] Error merging sessions:', error);
|
||||
res.status(500).json({
|
||||
error: 'Failed to merge sessions',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Mount the /claude sub-router
|
||||
router.use('/claude', claudeRouter);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user