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:
uroma
2026-01-22 14:43:05 +00:00
Unverified
parent b82837aa5f
commit 55aafbae9a
6463 changed files with 1115462 additions and 4486 deletions

638
routes/sessions-routes.js Normal file
View 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;