Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
7.6 kB · 210 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211import { mkdir, readFile, stat, unlink, writeFile } from 'fs/promises'import { dirname, join } from 'path'import { z } from 'zod/v4'import { logForDebugging } from '../utils/debug.js'import { isENOENT } from '../utils/errors.js'import { getWorktreePathsPortable } from '../utils/getWorktreePathsPortable.js'import { lazySchema } from '../utils/lazySchema.js'import { getProjectsDir, sanitizePath,} from '../utils/sessionStoragePortable.js'import { jsonParse, jsonStringify } from '../utils/slowOperations.js'
/** * Upper bound on worktree fanout. git worktree list is naturally bounded * (50 is a LOT), but this caps the parallel stat() burst and guards against * pathological setups. Above this, --continue falls back to current-dir-only. */const MAX_WORKTREE_FANOUT = 50
/** * Crash-recovery pointer for Remote Control sessions. * * Written immediately after a bridge session is created, periodically * refreshed during the session, and cleared on clean shutdown. If the * process dies unclean (crash, kill -9, terminal closed), the pointer * persists. On next startup, `claude remote-control` detects it and offers * to resume via the --session-id flow from #20460. * * Staleness is checked against the file's mtime (not an embedded timestamp) * so that a periodic re-write with the same content serves as a refresh — * matches the backend's rolling BRIDGE_LAST_POLL_TTL (4h) semantics. A * bridge that's been polling for 5+ hours and then crashes still has a * fresh pointer as long as the refresh ran within the window. * * Scoped per working directory (alongside transcript JSONL files) so two * concurrent bridges in different repos don't clobber each other. */
export const BRIDGE_POINTER_TTL_MS = 4 * 60 * 60 * 1000
const BridgePointerSchema = lazySchema(() => z.object({ sessionId: z.string(), environmentId: z.string(), source: z.enum(['standalone', 'repl']), }),)
export type BridgePointer = z.infer<ReturnType<typeof BridgePointerSchema>>
export function getBridgePointerPath(dir: string): string { return join(getProjectsDir(), sanitizePath(dir), 'bridge-pointer.json')}
/** * Write the pointer. Also used to refresh mtime during long sessions — * calling with the same IDs is a cheap no-content-change write that bumps * the staleness clock. Best-effort — a crash-recovery file must never * itself cause a crash. Logs and swallows on error. */export async function writeBridgePointer( dir: string, pointer: BridgePointer,): Promise<void> { const path = getBridgePointerPath(dir) try { await mkdir(dirname(path), { recursive: true }) await writeFile(path, jsonStringify(pointer), 'utf8') logForDebugging(`[bridge:pointer] wrote ${path}`) } catch (err: unknown) { logForDebugging(`[bridge:pointer] write failed: ${err}`, { level: 'warn' }) }}
/** * Read the pointer and its age (ms since last write). Operates directly * and handles errors — no existence check (CLAUDE.md TOCTOU rule). Returns * null on any failure: missing file, corrupted JSON, schema mismatch, or * stale (mtime > 4h ago). Stale/invalid pointers are deleted so they don't * keep re-prompting after the backend has already GC'd the env. */export async function readBridgePointer( dir: string,): Promise<(BridgePointer & { ageMs: number }) | null> { const path = getBridgePointerPath(dir) let raw: string let mtimeMs: number try { // stat for mtime (staleness anchor), then read. Two syscalls, but both // are needed — mtime IS the data we return, not a TOCTOU guard. mtimeMs = (await stat(path)).mtimeMs raw = await readFile(path, 'utf8') } catch { return null }
const parsed = BridgePointerSchema().safeParse(safeJsonParse(raw)) if (!parsed.success) { logForDebugging(`[bridge:pointer] invalid schema, clearing: ${path}`) await clearBridgePointer(dir) return null }
const ageMs = Math.max(0, Date.now() - mtimeMs) if (ageMs > BRIDGE_POINTER_TTL_MS) { logForDebugging(`[bridge:pointer] stale (>4h mtime), clearing: ${path}`) await clearBridgePointer(dir) return null }
return { ...parsed.data, ageMs }}
/** * Worktree-aware read for `--continue`. The REPL bridge writes its pointer * to `getOriginalCwd()` which EnterWorktreeTool/activeWorktreeSession can * mutate to a worktree path — but `claude remote-control --continue` runs * with `resolve('.')` = shell CWD. This fans out across git worktree * siblings to find the freshest pointer, matching /resume's semantics. * * Fast path: checks `dir` first. Only shells out to `git worktree list` if * that misses — the common case (pointer in launch dir) is one stat, zero * exec. Fanout reads run in parallel; capped at MAX_WORKTREE_FANOUT. * * Returns the pointer AND the dir it was found in, so the caller can clear * the right file on resume failure. */export async function readBridgePointerAcrossWorktrees( dir: string,): Promise<{ pointer: BridgePointer & { ageMs: number }; dir: string } | null> { // Fast path: current dir. Covers standalone bridge (always matches) and // REPL bridge when no worktree mutation happened. const here = await readBridgePointer(dir) if (here) { return { pointer: here, dir } }
// Fanout: scan worktree siblings. getWorktreePathsPortable has a 5s // timeout and returns [] on any error (not a git repo, git not installed). const worktrees = await getWorktreePathsPortable(dir) if (worktrees.length <= 1) return null if (worktrees.length > MAX_WORKTREE_FANOUT) { logForDebugging( `[bridge:pointer] ${worktrees.length} worktrees exceeds fanout cap ${MAX_WORKTREE_FANOUT}, skipping`, ) return null }
// Dedupe against `dir` so we don't re-stat it. sanitizePath normalizes // case/separators so worktree-list output matches our fast-path key even // on Windows where git may emit C:/ vs stored c:/. const dirKey = sanitizePath(dir) const candidates = worktrees.filter(wt => sanitizePath(wt) !== dirKey)
// Parallel stat+read. Each readBridgePointer is a stat() that ENOENTs // for worktrees with no pointer (cheap) plus a ~100-byte read for the // rare ones that have one. Promise.all → latency ≈ slowest single stat. const results = await Promise.all( candidates.map(async wt => { const p = await readBridgePointer(wt) return p ? { pointer: p, dir: wt } : null }), )
// Pick freshest (lowest ageMs). The pointer stores environmentId so // resume reconnects to the right env regardless of which worktree // --continue was invoked from. let freshest: { pointer: BridgePointer & { ageMs: number } dir: string } | null = null for (const r of results) { if (r && (!freshest || r.pointer.ageMs < freshest.pointer.ageMs)) { freshest = r } } if (freshest) { logForDebugging( `[bridge:pointer] fanout found pointer in worktree ${freshest.dir} (ageMs=${freshest.pointer.ageMs})`, ) } return freshest}
/** * Delete the pointer. Idempotent — ENOENT is expected when the process * shut down clean previously. */export async function clearBridgePointer(dir: string): Promise<void> { const path = getBridgePointerPath(dir) try { await unlink(path) logForDebugging(`[bridge:pointer] cleared ${path}`) } catch (err: unknown) { if (!isENOENT(err)) { logForDebugging(`[bridge:pointer] clear failed: ${err}`, { level: 'warn', }) } }}
function safeJsonParse(raw: string): unknown { try { return jsonParse(raw) } catch { return null }}