Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
6.8 kB · 204 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205import { feature } from 'bun:bundle'import { chmod, mkdir, readdir, readFile, unlink, writeFile } from 'fs/promises'import { join } from 'path'import { getOriginalCwd, getSessionId, onSessionSwitch,} from '../bootstrap/state.js'import { registerCleanup } from './cleanupRegistry.js'import { logForDebugging } from './debug.js'import { getClaudeConfigHomeDir } from './envUtils.js'import { errorMessage, isFsInaccessible } from './errors.js'import { isProcessRunning } from './genericProcessUtils.js'import { getPlatform } from './platform.js'import { jsonParse, jsonStringify } from './slowOperations.js'import { getAgentId } from './teammate.js'
export type SessionKind = 'interactive' | 'bg' | 'daemon' | 'daemon-worker'export type SessionStatus = 'busy' | 'idle' | 'waiting'
function getSessionsDir(): string { return join(getClaudeConfigHomeDir(), 'sessions')}
/** * Kind override from env. Set by the spawner (`claude --bg`, daemon * supervisor) so the child can register without the parent having to * write the file for it — cleanup-on-exit wiring then works for free. * Gated so the env-var string is DCE'd from external builds. */function envSessionKind(): SessionKind | undefined { if (feature('BG_SESSIONS')) { const k = process.env.CLAUDE_CODE_SESSION_KIND if (k === 'bg' || k === 'daemon' || k === 'daemon-worker') return k } return undefined}
/** * True when this REPL is running inside a `claude --bg` tmux session. * Exit paths (/exit, ctrl+c, ctrl+d) should detach the attached client * instead of killing the process. */export function isBgSession(): boolean { return envSessionKind() === 'bg'}
/** * Write a PID file for this session and register cleanup. * * Registers all top-level sessions — interactive CLI, SDK (vscode, desktop, * typescript, python, -p), bg/daemon spawns — so `claude ps` sees everything * the user might be running. Skips only teammates/subagents, which would * conflate swarm usage with genuine concurrency and pollute ps with noise. * * Returns true if registered, false if skipped. * Errors logged to debug, never thrown. */export async function registerSession(): Promise<boolean> { if (getAgentId() != null) return false
const kind: SessionKind = envSessionKind() ?? 'interactive' const dir = getSessionsDir() const pidFile = join(dir, `${process.pid}.json`)
registerCleanup(async () => { try { await unlink(pidFile) } catch { // ENOENT is fine (already deleted or never written) } })
try { await mkdir(dir, { recursive: true, mode: 0o700 }) await chmod(dir, 0o700) await writeFile( pidFile, jsonStringify({ pid: process.pid, sessionId: getSessionId(), cwd: getOriginalCwd(), startedAt: Date.now(), kind, entrypoint: process.env.CLAUDE_CODE_ENTRYPOINT, ...(feature('UDS_INBOX') ? { messagingSocketPath: process.env.CLAUDE_CODE_MESSAGING_SOCKET } : {}), ...(feature('BG_SESSIONS') ? { name: process.env.CLAUDE_CODE_SESSION_NAME, logPath: process.env.CLAUDE_CODE_SESSION_LOG, agent: process.env.CLAUDE_CODE_AGENT, } : {}), }), ) // --resume / /resume mutates getSessionId() via switchSession. Without // this, the PID file's sessionId goes stale and `claude ps` sparkline // reads the wrong transcript. onSessionSwitch(id => { void updatePidFile({ sessionId: id }) }) return true } catch (e) { logForDebugging(`[concurrentSessions] register failed: ${errorMessage(e)}`) return false }}
/** * Update this session's name in its PID registry file so ListPeers * can surface it. Best-effort: silently no-op if name is falsy, the * file doesn't exist (session not registered), or read/write fails. */async function updatePidFile(patch: Record<string, unknown>): Promise<void> { const pidFile = join(getSessionsDir(), `${process.pid}.json`) try { const data = jsonParse(await readFile(pidFile, 'utf8')) as Record< string, unknown > await writeFile(pidFile, jsonStringify({ ...data, ...patch })) } catch (e) { logForDebugging( `[concurrentSessions] updatePidFile failed: ${errorMessage(e)}`, ) }}
export async function updateSessionName( name: string | undefined,): Promise<void> { if (!name) return await updatePidFile({ name })}
/** * Record this session's Remote Control session ID so peer enumeration can * dedup: a session reachable over both UDS and bridge should only appear * once (local wins). Cleared on bridge teardown so stale IDs don't * suppress a legitimately-remote session after reconnect. */export async function updateSessionBridgeId( bridgeSessionId: string | null,): Promise<void> { await updatePidFile({ bridgeSessionId })}
/** * Push live activity state for `claude ps`. Fire-and-forget from REPL's * status-change effect — a dropped write just means ps falls back to * transcript-tail derivation for one refresh. */export async function updateSessionActivity(patch: { status?: SessionStatus waitingFor?: string}): Promise<void> { if (!feature('BG_SESSIONS')) return await updatePidFile({ ...patch, updatedAt: Date.now() })}
/** * Count live concurrent CLI sessions (including this one). * Filters out stale PID files (crashed sessions) and deletes them. * Returns 0 on any error (conservative). */export async function countConcurrentSessions(): Promise<number> { const dir = getSessionsDir() let files: string[] try { files = await readdir(dir) } catch (e) { if (!isFsInaccessible(e)) { logForDebugging(`[concurrentSessions] readdir failed: ${errorMessage(e)}`) } return 0 }
let count = 0 for (const file of files) { // Strict filename guard: only `<pid>.json` is a candidate. parseInt's // lenient prefix-parsing means `2026-03-14_notes.md` would otherwise // parse as PID 2026 and get swept as stale — silent user data loss. // See anthropics/claude-code#34210. if (!/^\d+\.json$/.test(file)) continue const pid = parseInt(file.slice(0, -5), 10) if (pid === process.pid) { count++ continue } if (isProcessRunning(pid)) { count++ } else if (getPlatform() !== 'wsl') { // Stale file from a crashed session — sweep it. Skip on WSL: if // ~/.claude/sessions/ is shared with Windows-native Claude (symlink // or CLAUDE_CONFIG_DIR), a Windows PID won't be probeable from WSL // and we'd falsely delete a live session's file. This is just // telemetry so conservative undercount is acceptable. void unlink(join(dir, file)).catch(() => {}) } } return count}