* fix: comprehensive Windows compatibility and gateway management improvements This commit addresses multiple Windows compatibility issues and improves gateway management across all platforms. ## Windows Compatibility Fixes - Add hermes-path.ts with cross-platform Hermes home/bin detection - Fix Windows native installation paths (%LOCALAPPDATA%\hermes) - Update terminal.ts to use PowerShell instead of /bin/bash on Windows - Fix upload.ts path construction to use path.join() for cross-platform paths - Fix download.ts to use isAbsolute() for Windows absolute path detection - Update auth.ts to skip file mode 0o600 on Windows (unsupported) - Add nodemon.json for cross-platform environment variable handling ## Gateway Management Improvements - Simplify gateway startup: all platforms use 'run' mode uniformly - Remove complex init system detection and platform-specific code paths - Improve PID file validation: use health check instead of port detection - Remove getPortByPid() method (too complex and error-prone) - Remove checkPortAvailable() TCP bind test (TIME_WAIT false positives) - Trust gateway --replace flag to handle real port conflicts - Add smart PID validation: check if stale process via health check - Fix port allocation to avoid incrementing when gateway restarts - Add allocatedPorts.clear() on each startAll() call - Add clearPidFile() method to clean up stale PID files ## Process Management - Remove detached:true and unref() from gateway spawn - Gateway processes now follow parent process lifecycle - Add process reference storage in ManagedGateway interface - Improve shutdown logic: call gatewayManager.stopAll() before exit - Fix Windows process killing: use process.kill(pid) for Windows - Remove PowerShell command for lock file cleanup (use Node.js fs.unlinkSync) ## Frontend Theme Fixes - Fix main.ts localStorage key mismatch (hermes_theme → hermes_brightness) - Add inline script in index.html to prevent FOUC (Flash of Unstyled Content) - Apply theme classes before Vue mount to avoid visual glitches ## Developer Experience - Fix nodemon windows-kill popup on Windows by removing signal config - Add delay and environment variables to nodemon.json - Add windowsHide: true to all child process spawns ## Breaking Changes - Gateway management now exclusively uses 'run' mode on all platforms - systemd/launchd integration removed (use --replace flag instead) This fix ensures hermes-web-ui works correctly on Windows native installations while maintaining compatibility with Linux/macOS/WSL2. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix gateway lifecycle port handling * fix: comprehensive Windows compatibility and gateway management improvements - Simplified hermes CLI binary resolution logic - Fixed Windows line ending compatibility in profile list parsing - Migrated gateway restart logic from CLI to GatewayManager - Added gateway restart to updateCredentials method - Removed unnecessary gateway restarts from provider operations - Fixed configuration preservation when switching profiles - Added nodemon quiet mode and legacy watch to reduce Windows popups Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * revert: change back to nodemon due to tsx compatibility issues - tsx has compatibility issues with Koa generator functions - Restored nodemon with simplified configuration - Added cross-env package for future Windows environment variable needs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: replace nodemon with ts-node-dev to eliminate Windows popup windows - Installed ts-node-dev as nodemon replacement - ts-node-dev has better Windows compatibility without console popups - Supports respawning, inspector debugging, and TypeScript compilation - Uses cross-env for Windows environment variable support - Removed nodemon.json configuration file (no longer needed) Benefits: - No more Windows console popup windows during development - Faster restart times compared to nodemon - Built-in TypeScript compilation without ts-node overhead Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: improve log parsing and Windows compatibility for agent/error logs - Fixed Pino JSON log parsing bug where logger field incorrectly used obj.msg - Changed logger field to use obj.name to properly display log source - Added Windows line ending support (\r\n) for log file listing - Added support for 'error' log type in addition to 'errors' - Improved error message extraction from obj.err when available This fixes the missing agent and error logs issue on Windows. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix gateway health checks and shutdown ownership * Refine auth lock window and dev shutdown * fix: improve Hermes plugin discovery on Windows by fixing Python path resolution - Added support for Windows venv Scripts directory structure - Fixed Python executable path detection for hermes.exe in venv/Scripts/ - Added Windows LOCALAPPDATA hermes-agent directory to search paths - Improved cross-platform compatibility for plugin discovery This fixes the "No module named 'hermes_cli'" error on Windows by correctly locating the Python virtual environment that contains the Hermes modules. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: improve cross-platform compatibility for Hermes plugin discovery - Added platform detection to only add Windows-specific paths on Windows - Prevents potential issues on Unix/Linux/macOS systems - Ensures LOCALAPPDATA path is only used when available on Windows - Maintains existing behavior for all platforms This makes the Windows plugin discovery fix safer for cross-platform usage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove unused development dependencies - Removed nodemon (replaced by ts-node-dev) - Removed tsx (had compatibility issues with Koa) - Removed nodemon.json configuration file - Cleaned up development tools to only what's actually used This reduces dependency size and eliminates the windows-kill popup source that was part of nodemon. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove memory system files - Removed MEMORY.md index file - Removed memory/ directory and windows-compatibility.md - Cleaned up unused memory persistence system Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: resolve TypeScript compilation error in plugins.ts - Added type assertion 'as string[]' after filter(Boolean) - Fixes TS2769 error: No overload matches this call - Ensures type compatibility with hasHermesPluginModule function Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: comprehensive Windows compatibility and gateway management improvements - Fix gateway detection after nodemon restart by adding health check-based detection - Prevent port conflicts by detecting already-running gateways without PID files - Switch to serial gateway startup to avoid lock file race conditions - Return to nodemon from ts-node-dev for development stability - Always stop gateways on shutdown to prevent orphan processes - Prevent project root config files from being committed to git - Fix syntax issues in plugins.ts Resolves issues where default profile gateway failed to start after nodemon restart and gateways were incorrectly marked as stopped despite running on correct ports. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: comic theme multilingual fonts, sidebar collapse fix, plugin discovery for Termux, and cron history - Add Chinese (ZCOOL KuaiLe), Japanese (Zen Maru Gothic), Korean (Gaegu) handwritten fonts for Comic theme - Fix collapsed sidebar: hide language switch, stack theme icons vertically - Add hermes shebang parsing as fallback Python discovery for Termux - Remove cron source filter from history sessions - Add 0.5.17 changelog entries for all 8 locales Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: tolerate duplicate YAML keys in config parsing (closes #628) Add `{ json: true }` to all 7 `yaml.load()` calls so duplicated mapping keys (e.g. multiple `mcp_servers:` blocks) no longer crash the API. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: gateway ownership check requires PID file to prevent cross-profile port hijacking Remove fallback that assumed ownership of healthy gateways without PID verification. Now only claims a gateway if PID file exists and process is alive, preventing one profile from hijacking another's port. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
192 lines
6.8 KiB
TypeScript
192 lines
6.8 KiB
TypeScript
/**
|
|
* Sync Hermes sessions from all profiles on startup.
|
|
* Reads api_server sessions from Hermes state.db and imports into local DB.
|
|
* Only runs when local DB is empty (first startup).
|
|
*
|
|
* Uses sessions-db.ts query logic to properly aggregate session chains.
|
|
*/
|
|
import { readdirSync, existsSync } from 'fs'
|
|
import { resolve, join } from 'path'
|
|
import { homedir } from 'os'
|
|
import { randomBytes } from 'crypto'
|
|
import { getProfileDir } from './hermes-profile'
|
|
import { createSession, addMessage, updateSession } from '../../db/hermes/session-store'
|
|
import { getDb } from '../../db/index'
|
|
import { logger } from '../logger'
|
|
import { listSessionSummaries as listHermesSessionSummaries } from '../../db/hermes/sessions-db'
|
|
import { detectHermesHome } from './hermes-path'
|
|
|
|
const HERMES_BASE = detectHermesHome()
|
|
const PROFILES_DIR = join(HERMES_BASE, 'profiles')
|
|
|
|
/**
|
|
* Generate a UUID v4 without external dependencies
|
|
*/
|
|
function generateUuid(): string {
|
|
const bytes = randomBytes(16)
|
|
bytes[6] = (bytes[6]! & 0x0f) | 0x40 // Version 4
|
|
bytes[8] = (bytes[8]! & 0x3f) | 0x80 // Variant 10
|
|
return [
|
|
bytes.subarray(0, 4).toString('hex'),
|
|
bytes.subarray(4, 6).toString('hex'),
|
|
bytes.subarray(6, 8).toString('hex'),
|
|
bytes.subarray(8, 10).toString('hex'),
|
|
bytes.subarray(10, 16).toString('hex'),
|
|
].join('-')
|
|
}
|
|
|
|
/**
|
|
* Get all available profile names including 'default'
|
|
*/
|
|
function getAllProfiles(): string[] {
|
|
const profiles = ['default']
|
|
|
|
if (existsSync(PROFILES_DIR)) {
|
|
const dirs = readdirSync(PROFILES_DIR, { withFileTypes: true })
|
|
.filter(dirent => dirent.isDirectory())
|
|
.map(dirent => dirent.name)
|
|
profiles.push(...dirs)
|
|
}
|
|
|
|
return profiles
|
|
}
|
|
|
|
/**
|
|
* Sync api_server sessions from a single profile.
|
|
* Uses sessions-db.ts query logic to properly aggregate session chains.
|
|
*/
|
|
async function syncProfileSessions(profile: string): Promise<{
|
|
synced: number
|
|
errors: string[]
|
|
}> {
|
|
const result = { synced: 0, errors: [] as string[] }
|
|
|
|
try {
|
|
// Use listSessionSummaries to get aggregated session chains
|
|
// This returns only root sessions with aggregated stats from the entire chain
|
|
const summaries = await listHermesSessionSummaries('api_server', 10000, profile)
|
|
|
|
logger.info(`[session-sync] profile '${profile}': found ${summaries.length} aggregated session chains`)
|
|
|
|
for (const hermesSession of summaries) {
|
|
// Skip ephemeral sessions (created internally by chat-run-socket)
|
|
if (hermesSession.id.startsWith('eph_')) continue
|
|
try {
|
|
// Generate new session ID for local DB
|
|
const newSessionId = generateUuid()
|
|
|
|
// Create session in local DB
|
|
createSession({
|
|
id: newSessionId,
|
|
profile,
|
|
model: hermesSession.model,
|
|
title: hermesSession.title || undefined,
|
|
})
|
|
|
|
// Get full detail including all messages from the session chain
|
|
const { getSessionDetailFromDbWithProfile } = await import('../../db/hermes/sessions-db')
|
|
const detail = await getSessionDetailFromDbWithProfile(hermesSession.id, profile)
|
|
|
|
if (!detail || !detail.messages) {
|
|
result.errors.push(`session ${hermesSession.id}: failed to load messages`)
|
|
logger.warn(`[session-sync] failed to load messages for session ${hermesSession.id}`)
|
|
continue
|
|
}
|
|
|
|
// Insert all messages from the entire chain
|
|
for (const msg of detail.messages) {
|
|
addMessage({
|
|
session_id: newSessionId,
|
|
role: msg.role,
|
|
content: msg.content,
|
|
tool_call_id: msg.tool_call_id,
|
|
tool_calls: msg.tool_calls,
|
|
tool_name: msg.tool_name,
|
|
timestamp: msg.timestamp,
|
|
token_count: msg.token_count,
|
|
finish_reason: msg.finish_reason,
|
|
reasoning: msg.reasoning,
|
|
reasoning_details: msg.reasoning_details,
|
|
reasoning_content: msg.reasoning_content,
|
|
})
|
|
}
|
|
|
|
// Update session with aggregated stats from Hermes
|
|
updateSession(newSessionId, {
|
|
started_at: hermesSession.started_at,
|
|
ended_at: hermesSession.ended_at,
|
|
end_reason: hermesSession.end_reason,
|
|
input_tokens: hermesSession.input_tokens,
|
|
output_tokens: hermesSession.output_tokens,
|
|
cache_read_tokens: hermesSession.cache_read_tokens,
|
|
cache_write_tokens: hermesSession.cache_write_tokens,
|
|
reasoning_tokens: hermesSession.reasoning_tokens,
|
|
estimated_cost_usd: hermesSession.estimated_cost_usd,
|
|
last_active: hermesSession.last_active,
|
|
preview: hermesSession.preview,
|
|
})
|
|
|
|
result.synced++
|
|
logger.info(`[session-sync] synced Hermes session ${hermesSession.id} -> ${newSessionId} (${detail.messages.length} messages, thread_session_count=${detail.thread_session_count})`)
|
|
} catch (err: any) {
|
|
result.errors.push(`session ${hermesSession.id}: ${err.message}`)
|
|
logger.warn(err, `[session-sync] failed to sync session ${hermesSession.id}`)
|
|
}
|
|
}
|
|
} catch (err: any) {
|
|
if (!err.message.includes('state.db not found')) {
|
|
result.errors.push(err.message)
|
|
logger.warn(err, `[session-sync] failed to open state.db for profile '${profile}'`)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Main entry point: sync all profiles on startup
|
|
* Only runs if local DB is empty (first startup or after DB reset)
|
|
*/
|
|
export async function syncAllHermesSessionsOnStartup(): Promise<void> {
|
|
// Check if local DB has any sessions - only sync if completely empty
|
|
const db = getDb()
|
|
if (!db) {
|
|
logger.info('[session-sync] SQLite not available, skipping Hermes sync')
|
|
return
|
|
}
|
|
|
|
const countResult = db.prepare('SELECT COUNT(*) as count FROM sessions').get() as { count: number } | undefined
|
|
const hasExistingSessions = countResult && countResult.count > 0
|
|
|
|
if (hasExistingSessions) {
|
|
logger.info('[session-sync] local DB has %d sessions, skipping Hermes sync', countResult!.count)
|
|
return
|
|
}
|
|
|
|
logger.info('[session-sync] local DB is empty, starting Hermes session sync...')
|
|
|
|
const profiles = getAllProfiles()
|
|
logger.info(`[session-sync] found ${profiles.length} profiles: ${profiles.join(', ')}`)
|
|
|
|
let totalSynced = 0
|
|
let totalErrors = 0
|
|
|
|
for (const profile of profiles) {
|
|
const result = await syncProfileSessions(profile)
|
|
totalSynced += result.synced
|
|
totalErrors += result.errors.length
|
|
|
|
if (result.errors.length > 0) {
|
|
logger.warn(`[session-sync] profile '${profile}' had ${result.errors.length} errors`)
|
|
for (const err of result.errors.slice(0, 5)) {
|
|
logger.warn(`[session-sync] - ${err}`)
|
|
}
|
|
if (result.errors.length > 5) {
|
|
logger.warn(`[session-sync] - ... and ${result.errors.length - 5} more errors`)
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.info(`[session-sync] sync complete: synced=${totalSynced}, errors=${totalErrors}`)
|
|
}
|