@dollhousemcp/mcp-server
Version:
DollhouseMCP - A Model Context Protocol (MCP) server that enables dynamic AI persona management from markdown files, allowing Claude and other compatible AI assistants to activate and switch between different behavioral personas.
593 lines • 101 kB
JavaScript
/**
* Unified web console orchestrator.
*
* Ties together leader election, console startup, follower wiring,
* and session lifecycle management. This is the main entry point
* called by the DI container during deferred setup.
*
* Flow:
* 1. Run leader election (read lock file, claim or follow)
* 2. If leader: start web server on fixed port, mount ingest routes, start heartbeat
* 3. If follower: register forwarding sinks with LogManager, start session heartbeat
*
* @since v2.1.0 — Issue #1700
*/
import { UnicodeValidator } from '../../security/validators/unicodeValidator.js';
import { logger } from '../../utils/logger.js';
import { electLeader, isLeaderWebConsoleReachable, forceClaimLeadership, startHeartbeat, registerLeaderCleanup, detectLegacyLeader, readLeaderLock, deleteLeaderLock, claimLeadership, createLeaderInfo, LOCK_VERSION, CONSOLE_PROTOCOL_VERSION, LEGACY_SERVER_VERSION, evaluateLeaderPreference, } from './LeaderElection.js';
import { createIngestRoutes } from './IngestRoutes.js';
import { LeaderForwardingLogSink, SessionHeartbeat, } from './LeaderForwardingSink.js';
import { PromotionManager } from './PromotionManager.js';
import { ConsoleTokenStore } from './consoleToken.js';
import { detectSessionClientPlatformId } from './sessionClientPlatform.js';
import { findPidOnPort, killStaleProcessDetailed, } from './StaleProcessRecovery.js';
import { env } from '../../config/env.js';
/**
* Default console port from the env var. Used as fallback when no port
* is provided via config file or options. The resolution hierarchy is:
* 1. options.port (from config file, resolved by the DI container)
* 2. DOLLHOUSE_WEB_CONSOLE_PORT env var
* 3. 41715 (hardcoded default in env.ts)
*/
const DEFAULT_CONSOLE_PORT = env.DOLLHOUSE_WEB_CONSOLE_PORT;
const LEGACY_CONSOLE_FALLBACK_PORT = 3939;
const SYNTHETIC_PORT_OWNER_SESSION_PREFIX = 'port-owner-';
const LEADER_DISCOVERY_TIMEOUT_MS = 2_000;
function currentTimestamp() {
return new Date().toISOString();
}
/**
* Check for a running legacy (pre-authentication) DollhouseMCP console and
* log a WARN-level message if one is found (#1794).
*
* Extracted from `startUnifiedConsole` so the wiring can be integration-
* tested in isolation without spinning up a full web server and leader
* election. The implementation is fire-and-forget: detection failures
* are logged at DEBUG and never propagate, because a failure here must
* not block leader election of the authenticated console.
*
* @param currentPort - The port the authenticated console intends to
* bind to. Used in the warning message to help the
* user tell the two consoles apart.
* @param detect - Optional injection point for the detection
* function. Defaults to `detectLegacyLeader`. Tests
* pass a stub.
* @param log - Optional injection point for the logger. Defaults
* to the module logger. Tests pass a spy.
* @returns The legacy leader info from `detect()`, or null if detection
* threw. Exposed so tests can assert the full result shape.
*/
export async function warnIfLegacyConsolePresent(currentPort, detect = detectLegacyLeader, log = logger) {
try {
const legacy = await detect();
if (legacy.legacyRunning) {
log.warn(`[UnifiedConsole] Legacy (pre-authentication) DollhouseMCP console detected ` +
`(pid=${legacy.pid}, port=${legacy.port}). Both consoles will run ` +
`independently on different ports with different security posture. ` +
`The authenticated console (this process) uses port ${currentPort}; ` +
`the legacy console uses port ${legacy.port ?? LEGACY_CONSOLE_FALLBACK_PORT}. ` +
`For consistent security, update the legacy installation to a ` +
`version with the authenticated console.`);
}
return legacy;
}
catch (err) {
// Best-effort — never block election on a detection failure
log.debug('[UnifiedConsole] Legacy leader detection failed', {
error: err instanceof Error ? err.message : String(err),
});
return null;
}
}
function buildDiscoveryHeaders(authToken) {
return authToken ? { Authorization: `Bearer ${authToken}` } : {};
}
function buildLeaderInfoFromSession(port, ownerPid, leaderSession) {
return {
version: LOCK_VERSION,
pid: ownerPid,
port,
sessionId: UnicodeValidator.normalize(leaderSession.sessionId).normalizedContent,
startedAt: leaderSession.startedAt ?? currentTimestamp(),
heartbeat: leaderSession.lastHeartbeat ?? currentTimestamp(),
serverVersion: leaderSession.serverVersion ?? LEGACY_SERVER_VERSION,
consoleProtocolVersion: leaderSession.consoleProtocolVersion ?? CONSOLE_PROTOCOL_VERSION,
};
}
function buildSyntheticLeaderInfo(port, ownerPid) {
const now = currentTimestamp();
return {
version: LOCK_VERSION,
pid: ownerPid,
port,
sessionId: `${SYNTHETIC_PORT_OWNER_SESSION_PREFIX}${ownerPid}`,
startedAt: now,
heartbeat: now,
serverVersion: LEGACY_SERVER_VERSION,
consoleProtocolVersion: CONSOLE_PROTOCOL_VERSION,
};
}
async function discoverLeaderViaSessionsApi(port, ownerPid, authToken, fetchImpl) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), LEADER_DISCOVERY_TIMEOUT_MS);
try {
const response = await fetchImpl(`http://127.0.0.1:${port}/api/sessions`, {
headers: buildDiscoveryHeaders(authToken),
signal: controller.signal,
});
if (!response.ok) {
return null;
}
const payload = await response.json();
const sessions = Array.isArray(payload.sessions) ? payload.sessions : [];
const leaderSession = sessions.find((session) => session.pid === ownerPid &&
session.isLeader === true &&
session.kind === 'mcp' &&
session.status !== 'stopped');
return leaderSession ? buildLeaderInfoFromSession(port, ownerPid, leaderSession) : null;
}
finally {
clearTimeout(timeout);
}
}
export async function discoverLeaderServingPort(port, authToken, deps = {}) {
const fetchImpl = deps.fetchImpl ?? fetch;
const findPidOnPortImpl = deps.findPidOnPortImpl ?? findPidOnPort;
const readLeaderLockImpl = deps.readLeaderLockImpl ?? readLeaderLock;
const ownerPid = await findPidOnPortImpl(port);
if (ownerPid !== null) {
try {
const leaderInfo = await discoverLeaderViaSessionsApi(port, ownerPid, authToken, fetchImpl);
if (leaderInfo) {
return { ownerPid, source: 'api', leaderInfo };
}
}
catch (err) {
logger.debug('[UnifiedConsole] Failed to query active leader sessions', {
port,
ownerPid,
error: err instanceof Error ? err.message : String(err),
});
}
}
const lock = await readLeaderLockImpl();
if (lock?.port === port && (ownerPid === null || lock.pid === ownerPid)) {
return {
ownerPid: ownerPid ?? lock.pid,
source: 'lock',
leaderInfo: {
...lock,
sessionId: UnicodeValidator.normalize(lock.sessionId).normalizedContent,
},
};
}
if (ownerPid !== null) {
return {
ownerPid,
source: 'synthetic',
leaderInfo: buildSyntheticLeaderInfo(port, ownerPid),
};
}
return { leaderInfo: null, ownerPid: null, source: 'none' };
}
export async function recoverLeaderBindFailure(provisionalLeader, port, authToken, deps = {}) {
const readLeaderLockImpl = deps.readLeaderLockImpl ?? readLeaderLock;
const deleteLeaderLockImpl = deps.deleteLeaderLockImpl ?? deleteLeaderLock;
logger.info('[UnifiedConsole] Leader bind recovery initiated', {
provisionalSessionId: provisionalLeader.sessionId,
provisionalPid: provisionalLeader.pid,
port,
});
let fallback = await discoverLeaderServingPort(port, authToken, deps);
let lockCleanupAttempted = false;
let lockCleanupPerformed = false;
const currentLock = await readLeaderLockImpl();
const provisionalLockMatches = (currentLock?.pid === provisionalLeader.pid &&
currentLock.port === provisionalLeader.port &&
currentLock.sessionId === provisionalLeader.sessionId);
const fallbackPointsToProvisionalLeader = (fallback.leaderInfo?.pid === provisionalLeader.pid &&
fallback.leaderInfo.port === provisionalLeader.port &&
fallback.leaderInfo.sessionId === provisionalLeader.sessionId);
if (provisionalLockMatches) {
lockCleanupAttempted = true;
await deleteLeaderLockImpl();
lockCleanupPerformed = true;
logger.info('[UnifiedConsole] Removed provisional leader lock after bind failure', {
provisionalSessionId: provisionalLeader.sessionId,
provisionalPid: provisionalLeader.pid,
port,
});
if (fallbackPointsToProvisionalLeader) {
fallback = await discoverLeaderServingPort(port, authToken, deps);
}
}
logger.info('[UnifiedConsole] Leader bind recovery completed', {
provisionalSessionId: provisionalLeader.sessionId,
provisionalPid: provisionalLeader.pid,
port,
discoverySource: fallback.source,
ownerPid: fallback.ownerPid,
lockCleanupAttempted,
lockCleanupPerformed,
});
return {
...fallback,
lockCleanupAttempted,
lockCleanupPerformed,
};
}
export function evaluatePortOwnerReplacement(candidateLeader, fallback) {
if (!fallback.leaderInfo || fallback.ownerPid === null || fallback.ownerPid === candidateLeader.pid) {
return {
shouldEvict: false,
ownerPid: fallback.ownerPid,
preference: null,
};
}
const preference = evaluateLeaderPreference(candidateLeader, fallback.leaderInfo);
return {
shouldEvict: preference.shouldReplace,
ownerPid: fallback.ownerPid,
preference,
};
}
function buildBindFailureLogContext(consolePort, provisionalLeader, bindResult, fallback, replacement, forcedKill) {
return {
port: consolePort,
bindError: bindResult?.error,
bindDetail: bindResult?.detail,
provisionalLeaderPid: provisionalLeader.pid,
provisionalLeaderSessionId: provisionalLeader.sessionId,
provisionalLeaderVersion: provisionalLeader.serverVersion ?? LEGACY_SERVER_VERSION,
provisionalLeaderProtocolVersion: provisionalLeader.consoleProtocolVersion ?? CONSOLE_PROTOCOL_VERSION,
fallbackOwnerPid: fallback.ownerPid,
fallbackSource: fallback.source,
fallbackLeaderPid: fallback.leaderInfo?.pid,
fallbackLeaderSessionId: fallback.leaderInfo?.sessionId,
fallbackLeaderVersion: fallback.leaderInfo?.serverVersion ?? LEGACY_SERVER_VERSION,
fallbackLeaderProtocolVersion: fallback.leaderInfo?.consoleProtocolVersion ?? CONSOLE_PROTOCOL_VERSION,
replacementShouldEvict: replacement?.shouldEvict ?? false,
replacementReason: replacement?.preference?.reason,
forcedKillReason: forcedKill?.reason,
forcedKillPid: forcedKill?.pid,
forcedKillDetail: forcedKill?.detail,
};
}
function buildAuthorityResolutionLogContext(consolePort, electedLeader, discovery, replacement) {
return {
port: consolePort,
electedLeaderPid: electedLeader.pid,
electedLeaderSessionId: electedLeader.sessionId,
electedLeaderVersion: electedLeader.serverVersion ?? LEGACY_SERVER_VERSION,
electedLeaderProtocolVersion: electedLeader.consoleProtocolVersion ?? CONSOLE_PROTOCOL_VERSION,
servingOwnerPid: discovery?.ownerPid ?? null,
servingSource: discovery?.source ?? 'none',
servingLeaderPid: discovery?.leaderInfo?.pid ?? null,
servingLeaderSessionId: discovery?.leaderInfo?.sessionId ?? null,
servingLeaderVersion: discovery?.leaderInfo?.serverVersion ?? LEGACY_SERVER_VERSION,
servingLeaderProtocolVersion: discovery?.leaderInfo?.consoleProtocolVersion ?? CONSOLE_PROTOCOL_VERSION,
replacementShouldEvict: replacement?.shouldEvict ?? false,
replacementReason: replacement?.preference?.reason ?? null,
};
}
export async function resolveFollowerAuthority(sessionId, consolePort, election, deps = {}) {
const isLeaderWebConsoleReachableImpl = deps.isLeaderWebConsoleReachableImpl ?? isLeaderWebConsoleReachable;
const discoverLeaderServingPortImpl = deps.discoverLeaderServingPortImpl ?? discoverLeaderServingPort;
const forceClaimLeadershipImpl = deps.forceClaimLeadershipImpl ?? forceClaimLeadership;
const deleteLeaderLockImpl = deps.deleteLeaderLockImpl ?? deleteLeaderLock;
const reachable = await isLeaderWebConsoleReachableImpl(election.leaderInfo);
if (!reachable) {
logger.warn('[UnifiedConsole] Elected leader is not serving the console port; forcing takeover', {
port: consolePort,
electedLeaderPid: election.leaderInfo.pid,
electedLeaderSessionId: election.leaderInfo.sessionId,
});
return {
election: await forceClaimLeadershipImpl(sessionId, consolePort),
discovery: null,
replacement: null,
forcedClaim: true,
};
}
const candidateLeader = createLeaderInfo(sessionId, consolePort);
const discovery = await discoverLeaderServingPortImpl(consolePort, null);
if (!discovery.leaderInfo || discovery.ownerPid === null) {
return {
election,
discovery,
replacement: null,
forcedClaim: false,
};
}
const replacement = evaluatePortOwnerReplacement(candidateLeader, discovery);
if (discovery.ownerPid !== election.leaderInfo.pid) {
if (replacement.shouldEvict) {
await deleteLeaderLockImpl();
logger.warn('[UnifiedConsole] Split-brain console authority detected; newer session will replace the actual port owner', buildAuthorityResolutionLogContext(consolePort, election.leaderInfo, discovery, replacement));
return {
election: { role: 'leader', leaderInfo: candidateLeader },
discovery,
replacement,
forcedClaim: false,
};
}
logger.warn('[UnifiedConsole] Split-brain console authority detected; following the actual port owner', buildAuthorityResolutionLogContext(consolePort, election.leaderInfo, discovery, replacement));
return {
election: { role: 'follower', leaderInfo: discovery.leaderInfo },
discovery,
replacement,
forcedClaim: false,
};
}
return {
election,
discovery,
replacement,
forcedClaim: false,
};
}
async function attemptForceTakeover(options, currentElection, consolePort, primaryToken, serverOpts, startWebServerImpl) {
const initialFallback = await recoverLeaderBindFailure(currentElection.leaderInfo, consolePort, primaryToken);
const initialReplacement = evaluatePortOwnerReplacement(currentElection.leaderInfo, initialFallback);
if (!initialReplacement.shouldEvict || initialReplacement.ownerPid === null) {
return {
webResult: { bindResult: { success: false, error: 'EADDRINUSE', detail: `Port ${consolePort} already in use` } },
election: currentElection,
fallback: initialFallback,
replacement: initialReplacement,
forcedKill: null,
takeoverAttempted: false,
reboundLockClaimed: false,
};
}
const latestFallback = await discoverLeaderServingPort(consolePort, primaryToken);
const latestReplacement = evaluatePortOwnerReplacement(currentElection.leaderInfo, latestFallback);
if (!latestReplacement.shouldEvict || latestReplacement.ownerPid === null) {
logger.warn('[UnifiedConsole] Forced takeover target changed before eviction; skipping forced kill', {
...buildBindFailureLogContext(consolePort, currentElection.leaderInfo, { success: false, error: 'EADDRINUSE', detail: `Port ${consolePort} already in use` }, latestFallback, latestReplacement),
previousOwnerPid: initialReplacement.ownerPid,
});
return {
webResult: { bindResult: { success: false, error: 'EADDRINUSE', detail: `Port ${consolePort} already in use` } },
election: currentElection,
fallback: latestFallback,
replacement: latestReplacement,
forcedKill: null,
takeoverAttempted: false,
reboundLockClaimed: false,
};
}
logger.warn('[UnifiedConsole] Attempting forced takeover from older or incompatible active leader', {
...buildBindFailureLogContext(consolePort, currentElection.leaderInfo, { success: false, error: 'EADDRINUSE', detail: `Port ${consolePort} already in use` }, latestFallback, latestReplacement),
});
const forcedKill = await killStaleProcessDetailed(latestReplacement.ownerPid, consolePort, {
allowActiveHostParent: true,
});
if (!forcedKill.killed) {
logger.warn('[UnifiedConsole] Forced takeover skipped or failed after identifying replaceable leader', {
...buildBindFailureLogContext(consolePort, currentElection.leaderInfo, { success: false, error: 'EADDRINUSE', detail: `Port ${consolePort} already in use` }, latestFallback, latestReplacement, forcedKill),
});
return {
webResult: { bindResult: { success: false, error: 'EADDRINUSE', detail: `Port ${consolePort} already in use` } },
election: currentElection,
fallback: latestFallback,
replacement: latestReplacement,
forcedKill,
takeoverAttempted: true,
reboundLockClaimed: false,
};
}
const reboundWebResult = await startWebServerImpl(serverOpts);
let reboundElection = currentElection;
let reboundLockClaimed = false;
if (!reboundWebResult.bindResult || reboundWebResult.bindResult.success) {
const reboundLeaderInfo = createLeaderInfo(options.sessionId, consolePort);
reboundLockClaimed = await claimLeadership(reboundLeaderInfo);
if (!reboundLockClaimed) {
logger.warn('[UnifiedConsole] Rebound leader bound port but could not immediately re-claim lock', {
...buildBindFailureLogContext(consolePort, reboundLeaderInfo, reboundWebResult.bindResult, latestFallback, latestReplacement, forcedKill),
});
}
reboundElection = { role: 'leader', leaderInfo: reboundLeaderInfo };
}
else {
logger.warn('[UnifiedConsole] Forced takeover killed old leader but bind retry still failed', {
...buildBindFailureLogContext(consolePort, currentElection.leaderInfo, reboundWebResult.bindResult, latestFallback, latestReplacement, forcedKill),
});
}
return {
webResult: reboundWebResult,
election: reboundElection,
fallback: latestFallback,
replacement: latestReplacement,
forcedKill,
takeoverAttempted: true,
reboundLockClaimed,
};
}
/**
* Start the unified web console.
*
* Runs leader election, then either starts the full console (leader)
* or sets up event forwarding (follower).
*/
export async function startUnifiedConsole(options) {
// Resolve port: options (config file) → env var → default
const consolePort = options.port || DEFAULT_CONSOLE_PORT;
logger.debug(`[UnifiedConsole] Port resolved: ${consolePort}` +
(options.port ? ' (from config file)' : ` (from env/default)`));
// Legacy-leader detection (#1794) — warn the user if a pre-auth
// DollhouseMCP console is running alongside this authenticated one.
// They will coexist fine because of port + lock + token file isolation,
// but the user should know both exist so the differing security posture
// between them doesn't look like a bug.
await warnIfLegacyConsolePresent(consolePort);
let election = await electLeader(options.sessionId, consolePort);
if (election.role === 'follower') {
const resolved = await resolveFollowerAuthority(options.sessionId, consolePort, election);
election = resolved.election;
}
if (election.role === 'leader') {
return startAsLeader(options, election, consolePort);
}
else {
return startAsFollower(options, election, consolePort);
}
}
/**
* Start as the console leader.
* Binds the resolved console port (config file → env var → default),
* mounts all routes including ingestion, starts heartbeat.
*/
async function startAsLeader(options, election, consolePort = DEFAULT_CONSOLE_PORT) {
const clientPlatform = detectSessionClientPlatformId();
const { startWebServer } = await import('../server.js');
const { pickRandomTokenName } = await import('./SessionNames.js');
// Initialize the console token store (#1780). Creates the token file on
// first run, reads the existing tokens on subsequent runs. The token is
// persistent across restarts — only rotated on explicit request (Phase 2).
// Feature flag DOLLHOUSE_WEB_AUTH_ENABLED controls enforcement; the file
// is generated regardless so consumers can attach tokens preemptively.
const tokenStore = new ConsoleTokenStore(env.DOLLHOUSE_CONSOLE_TOKEN_FILE);
const primaryToken = await tokenStore.ensureInitialized(pickRandomTokenName());
logger.info('[UnifiedConsole] Console token store initialized', {
tokenId: primaryToken.id,
tokenName: primaryToken.name,
file: tokenStore.getFilePath(),
authEnforced: env.DOLLHOUSE_WEB_AUTH_ENABLED,
});
// Pre-create a placeholder broadcast that we'll wire up after the server starts
let liveBroadcast;
let liveMetricsOnSnapshot;
// Create ingestion routes with a deferred broadcast (wired after server starts)
const ingestResult = createIngestRoutes({
logBroadcast: (entry) => liveBroadcast?.(entry),
metricsOnSnapshot: (snapshot) => liveMetricsOnSnapshot?.(snapshot),
storeMetricsSnapshot: (snapshot) => options.metricsSink?.onSnapshot(snapshot),
});
// Start the web server with ingest routes mounted before the SPA fallback.
// If the port is occupied by a stale process, retry with exponential backoff.
const serverOpts = {
portfolioDir: options.portfolioDir,
memorySink: options.memorySink,
metricsSink: options.metricsSink,
port: consolePort,
sessionId: options.stableSessionId,
runtimeSessionId: options.sessionId,
additionalRouters: [ingestResult.router],
tokenStore,
...(options.mcpAqlHandler ? { mcpAqlHandler: options.mcpAqlHandler } : {}),
};
// bindAndListen now handles EADDRINUSE by finding and killing the stale
// process on the port, then retrying. No external retry loop needed.
let webResult = await startWebServer(serverOpts);
if (webResult.bindResult && !webResult.bindResult.success) {
const forceTakeover = await attemptForceTakeover(options, election, consolePort, primaryToken.token, serverOpts, startWebServer);
webResult = forceTakeover.webResult;
election = forceTakeover.election;
if (webResult.bindResult && !webResult.bindResult.success) {
if (forceTakeover.fallback.leaderInfo) {
logger.warn('[UnifiedConsole] Leader role aborted: bind failed, falling back to follower', {
...buildBindFailureLogContext(consolePort, election.leaderInfo, webResult.bindResult, forceTakeover.fallback, forceTakeover.replacement, forceTakeover.forcedKill),
takeoverAttempted: forceTakeover.takeoverAttempted,
reboundLockClaimed: forceTakeover.reboundLockClaimed,
lockCleanupAttempted: forceTakeover.fallback.source !== 'none',
});
const followerElection = { role: 'follower', leaderInfo: forceTakeover.fallback.leaderInfo };
return startAsFollower(options, followerElection, consolePort, primaryToken.token);
}
logger.error('[UnifiedConsole] Leader failed to bind and no active leader could be identified', {
...buildBindFailureLogContext(consolePort, election.leaderInfo, webResult.bindResult, forceTakeover.fallback, forceTakeover.replacement, forceTakeover.forcedKill),
takeoverAttempted: forceTakeover.takeoverAttempted,
reboundLockClaimed: forceTakeover.reboundLockClaimed,
});
throw new Error(`Leader failed to bind port ${consolePort} and no active leader was discoverable`);
}
}
// Register the leader only after the HTTP listener is actually serving the port.
ingestResult.registerLeaderSession(options.sessionId, process.pid, clientPlatform);
// Register the web console itself so the session indicator is never empty (#1805)
ingestResult.registerConsoleSession();
// Wire SSE broadcasts for this leader's own events
options.wireSSEBroadcasts(webResult, options.metricsSink);
// Now wire the live broadcast functions into the ingest routes
if (webResult.logBroadcast) {
const originalBroadcast = webResult.logBroadcast;
// Stamp leader's own entries with session ID
liveBroadcast = (entry) => {
const stamped = {
...entry,
data: { ...entry.data, _sessionId: options.sessionId },
};
originalBroadcast(stamped);
};
}
liveMetricsOnSnapshot = webResult.metricsOnSnapshot;
logger.info('[UnifiedConsole] Ingestion routes mounted');
// Start heartbeat and register cleanup
const stopHeartbeat = startHeartbeat(election.leaderInfo);
registerLeaderCleanup();
logger.info('[UnifiedConsole] Leader started', {
sessionId: options.sessionId, port: consolePort, pid: process.pid,
role: 'leader', ingestRoutes: ['/api/ingest/logs', '/api/ingest/metrics', '/api/ingest/session', '/api/sessions'],
});
return {
role: 'leader',
election,
port: consolePort,
cleanup: async () => {
stopHeartbeat();
},
};
}
/**
* Start as a follower.
* Registers forwarding sinks with the LogManager, starts session heartbeat.
*/
async function startAsFollower(options, election, consolePort = DEFAULT_CONSOLE_PORT, initialAuthToken = null) {
const clientPlatform = detectSessionClientPlatformId();
const leaderUrl = `http://127.0.0.1:${election.leaderInfo.port}`;
// Read the console auth token (#1780) written by the leader. May be null
// if the file doesn't exist yet — the sinks handle that gracefully and
// simply omit the Bearer header, which is fine when auth is not enforced.
let authToken = initialAuthToken;
if (authToken === null) {
const { getPrimaryTokenFromFile } = await import('./consoleToken.js');
authToken = await getPrimaryTokenFromFile(env.DOLLHOUSE_CONSOLE_TOKEN_FILE);
}
if (authToken) {
logger.debug('[UnifiedConsole] Follower loaded console auth token');
}
else {
logger.debug('[UnifiedConsole] No console auth token file found; follower will POST without Bearer header');
}
// Per-instance promotion manager — tracks its own attempt counter so
// multiple followers don't interfere with each other's promotion budgets.
const promotionMgr = new PromotionManager(options, consolePort, startAsLeader, startAsFollower);
// Declare sessionHeartbeat before the sink so the closure can capture it.
// Both are initialized before the callback could possibly fire (needs 5+ failed flushes).
let sessionHeartbeat;
// Register a forwarding log sink with leader-death callback (#1850).
const forwardingSink = new LeaderForwardingLogSink(leaderUrl, options.sessionId, authToken, () => {
promotionMgr.promote(forwardingSink, sessionHeartbeat)
.catch(err => logger.error('[UnifiedConsole] Promotion crashed', { error: String(err) }));
});
options.registerLogSink(forwardingSink);
// Start session heartbeat to the leader
sessionHeartbeat = new SessionHeartbeat(leaderUrl, options.sessionId, process.pid, authToken, clientPlatform);
await sessionHeartbeat.start();
logger.info('[UnifiedConsole] Follower started', {
sessionId: options.sessionId, pid: process.pid, role: 'follower',
leaderSession: election.leaderInfo.sessionId, leaderPid: election.leaderInfo.pid,
leaderPort: election.leaderInfo.port, leaderUrl,
});
return {
role: 'follower',
election,
cleanup: async () => {
await sessionHeartbeat.stop();
await forwardingSink.close();
},
};
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiVW5pZmllZENvbnNvbGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvd2ViL2NvbnNvbGUvVW5pZmllZENvbnNvbGUudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7R0FhRztBQU9ILE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLCtDQUErQyxDQUFDO0FBQ2pGLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUMvQyxPQUFPLEVBQ0wsV0FBVyxFQUNYLDJCQUEyQixFQUMzQixvQkFBb0IsRUFDcEIsY0FBYyxFQUNkLHFCQUFxQixFQUNyQixrQkFBa0IsRUFDbEIsY0FBYyxFQUNkLGdCQUFnQixFQUNoQixlQUFlLEVBQ2YsZ0JBQWdCLEVBQ2hCLFlBQVksRUFDWix3QkFBd0IsRUFDeEIscUJBQXFCLEVBQ3JCLHdCQUF3QixHQUl6QixNQUFNLHFCQUFxQixDQUFDO0FBQzdCLE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLG1CQUFtQixDQUFDO0FBQ3ZELE9BQU8sRUFDTCx1QkFBdUIsRUFDdkIsZ0JBQWdCLEdBQ2pCLE1BQU0sMkJBQTJCLENBQUM7QUFDbkMsT0FBTyxFQUFFLGdCQUFnQixFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDekQsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFDdEQsT0FBTyxFQUFFLDZCQUE2QixFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFDM0UsT0FBTyxFQUNMLGFBQWEsRUFDYix3QkFBd0IsR0FFekIsTUFBTSwyQkFBMkIsQ0FBQztBQUNuQyxPQUFPLEVBQUUsR0FBRyxFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFFMUM7Ozs7OztHQU1HO0FBQ0gsTUFBTSxvQkFBb0IsR0FBRyxHQUFHLENBQUMsMEJBQTBCLENBQUM7QUFDNUQsTUFBTSw0QkFBNEIsR0FBRyxJQUFJLENBQUM7QUFDMUMsTUFBTSxtQ0FBbUMsR0FBRyxh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