source dump of claude code
at main 133 lines 4.1 kB view raw
1/** 2 * Session activity tracking with refcount-based heartbeat timer. 3 * 4 * The transport registers its keep-alive sender via registerSessionActivityCallback(). 5 * Callers (API streaming, tool execution) bracket their work with 6 * startSessionActivity() / stopSessionActivity(). When the refcount is >0 a 7 * periodic timer fires the registered callback every 30 seconds to keep the 8 * container alive. 9 * 10 * Sending keep-alives is gated behind CLAUDE_CODE_REMOTE_SEND_KEEPALIVES. 11 * Diagnostic logging always fires to help diagnose idle gaps. 12 */ 13 14import { registerCleanup } from './cleanupRegistry.js' 15import { logForDiagnosticsNoPII } from './diagLogs.js' 16import { isEnvTruthy } from './envUtils.js' 17 18const SESSION_ACTIVITY_INTERVAL_MS = 30_000 19 20export type SessionActivityReason = 'api_call' | 'tool_exec' 21 22let activityCallback: (() => void) | null = null 23let refcount = 0 24const activeReasons = new Map<SessionActivityReason, number>() 25let oldestActivityStartedAt: number | null = null 26let heartbeatTimer: ReturnType<typeof setInterval> | null = null 27let idleTimer: ReturnType<typeof setTimeout> | null = null 28let cleanupRegistered = false 29 30function startHeartbeatTimer(): void { 31 clearIdleTimer() 32 heartbeatTimer = setInterval(() => { 33 logForDiagnosticsNoPII('debug', 'session_keepalive_heartbeat', { 34 refcount, 35 }) 36 if (isEnvTruthy(process.env.CLAUDE_CODE_REMOTE_SEND_KEEPALIVES)) { 37 activityCallback?.() 38 } 39 }, SESSION_ACTIVITY_INTERVAL_MS) 40} 41 42function startIdleTimer(): void { 43 clearIdleTimer() 44 if (activityCallback === null) { 45 return 46 } 47 idleTimer = setTimeout(() => { 48 logForDiagnosticsNoPII('info', 'session_idle_30s') 49 idleTimer = null 50 }, SESSION_ACTIVITY_INTERVAL_MS) 51} 52 53function clearIdleTimer(): void { 54 if (idleTimer !== null) { 55 clearTimeout(idleTimer) 56 idleTimer = null 57 } 58} 59 60export function registerSessionActivityCallback(cb: () => void): void { 61 activityCallback = cb 62 // Restart timer if work is already in progress (e.g. reconnect during streaming) 63 if (refcount > 0 && heartbeatTimer === null) { 64 startHeartbeatTimer() 65 } 66} 67 68export function unregisterSessionActivityCallback(): void { 69 activityCallback = null 70 // Stop timer if the callback is removed 71 if (heartbeatTimer !== null) { 72 clearInterval(heartbeatTimer) 73 heartbeatTimer = null 74 } 75 clearIdleTimer() 76} 77 78export function sendSessionActivitySignal(): void { 79 if (isEnvTruthy(process.env.CLAUDE_CODE_REMOTE_SEND_KEEPALIVES)) { 80 activityCallback?.() 81 } 82} 83 84export function isSessionActivityTrackingActive(): boolean { 85 return activityCallback !== null 86} 87 88/** 89 * Increment the activity refcount. When it transitions from 0→1 and a callback 90 * is registered, start a periodic heartbeat timer. 91 */ 92export function startSessionActivity(reason: SessionActivityReason): void { 93 refcount++ 94 activeReasons.set(reason, (activeReasons.get(reason) ?? 0) + 1) 95 if (refcount === 1) { 96 oldestActivityStartedAt = Date.now() 97 if (activityCallback !== null && heartbeatTimer === null) { 98 startHeartbeatTimer() 99 } 100 } 101 if (!cleanupRegistered) { 102 cleanupRegistered = true 103 registerCleanup(async () => { 104 logForDiagnosticsNoPII('info', 'session_activity_at_shutdown', { 105 refcount, 106 active: Object.fromEntries(activeReasons), 107 // Only meaningful while work is in-flight; stale otherwise. 108 oldest_activity_ms: 109 refcount > 0 && oldestActivityStartedAt !== null 110 ? Date.now() - oldestActivityStartedAt 111 : null, 112 }) 113 }) 114 } 115} 116 117/** 118 * Decrement the activity refcount. When it reaches 0, stop the heartbeat timer 119 * and start an idle timer that logs after 30s of inactivity. 120 */ 121export function stopSessionActivity(reason: SessionActivityReason): void { 122 if (refcount > 0) { 123 refcount-- 124 } 125 const n = (activeReasons.get(reason) ?? 0) - 1 126 if (n > 0) activeReasons.set(reason, n) 127 else activeReasons.delete(reason) 128 if (refcount === 0 && heartbeatTimer !== null) { 129 clearInterval(heartbeatTimer) 130 heartbeatTimer = null 131 startIdleTimer() 132 } 133}