Files
DeskClaw/electron/gateway/process-policy.ts
paisley 2c5c82bb74 Refactor clawx (#344)
Co-authored-by: ashione <skyzlxuan@gmail.com>
2026-03-09 13:10:42 +08:00

121 lines
3.6 KiB
TypeScript

export interface ReconnectConfig {
maxAttempts: number;
baseDelay: number;
maxDelay: number;
}
export const DEFAULT_RECONNECT_CONFIG: ReconnectConfig = {
maxAttempts: 10,
baseDelay: 1000,
maxDelay: 30000,
};
export function nextLifecycleEpoch(currentEpoch: number): number {
return currentEpoch + 1;
}
export function isLifecycleSuperseded(expectedEpoch: number, currentEpoch: number): boolean {
return expectedEpoch !== currentEpoch;
}
export interface ReconnectAttemptContext {
scheduledEpoch: number;
currentEpoch: number;
shouldReconnect: boolean;
}
export function getReconnectSkipReason(context: ReconnectAttemptContext): string | null {
if (!context.shouldReconnect) {
return 'auto-reconnect disabled';
}
if (isLifecycleSuperseded(context.scheduledEpoch, context.currentEpoch)) {
return `stale reconnect callback (scheduledEpoch=${context.scheduledEpoch}, currentEpoch=${context.currentEpoch})`;
}
return null;
}
export interface ReconnectScheduleContext {
shouldReconnect: boolean;
hasReconnectTimer: boolean;
reconnectAttempts: number;
maxAttempts: number;
baseDelay: number;
maxDelay: number;
}
export type ReconnectScheduleDecision =
| { action: 'skip'; reason: string }
| { action: 'already-scheduled' }
| { action: 'fail'; attempts: number; maxAttempts: number }
| { action: 'schedule'; nextAttempt: number; maxAttempts: number; delay: number };
export function getReconnectScheduleDecision(
context: ReconnectScheduleContext,
): ReconnectScheduleDecision {
if (!context.shouldReconnect) {
return { action: 'skip', reason: 'auto-reconnect disabled' };
}
if (context.hasReconnectTimer) {
return { action: 'already-scheduled' };
}
if (context.reconnectAttempts >= context.maxAttempts) {
return {
action: 'fail',
attempts: context.reconnectAttempts,
maxAttempts: context.maxAttempts,
};
}
const delay = Math.min(
context.baseDelay * Math.pow(2, context.reconnectAttempts),
context.maxDelay,
);
return {
action: 'schedule',
nextAttempt: context.reconnectAttempts + 1,
maxAttempts: context.maxAttempts,
delay,
};
}
export type GatewayLifecycleState = 'stopped' | 'starting' | 'running' | 'error' | 'reconnecting';
export interface RestartDeferralContext {
state: GatewayLifecycleState;
startLock: boolean;
}
/**
* Restart requests should not interrupt an in-flight startup/reconnect flow.
* Doing so can kill a just-spawned process and leave the manager stopped.
*/
export function shouldDeferRestart(context: RestartDeferralContext): boolean {
return context.startLock || context.state === 'starting' || context.state === 'reconnecting';
}
export interface DeferredRestartActionContext extends RestartDeferralContext {
hasPendingRestart: boolean;
shouldReconnect: boolean;
}
export type DeferredRestartAction = 'none' | 'wait' | 'drop' | 'execute';
/**
* Decide what to do with a pending deferred restart once lifecycle changes.
*
* A deferred restart is an explicit restart() call that was postponed because
* the manager was mid-startup/reconnect. When the in-flight operation settles
* we must honour the request — even if the gateway is now running — because
* the caller may have changed config (e.g. provider switch) that the current
* process hasn't picked up.
*/
export function getDeferredRestartAction(context: DeferredRestartActionContext): DeferredRestartAction {
if (!context.hasPendingRestart) return 'none';
if (shouldDeferRestart(context)) return 'wait';
if (!context.shouldReconnect) return 'drop';
return 'execute';
}