1 import Base 2 import ./fs.bend as Fs 3 import ./act.bend as Act 4 import ./clock.bend as Clock 5 import ./fs/write.bend as FsW 6 7 # Directory locks, the npm proper-lockfile protocol Claude Code uses for its 8 # credential and config files (contract 1.8): `mkdir` takes the lock, `rmdir` 9 # releases it, and a lock whose mtime is older than `staleness` seconds is 10 # taken over. cswap holds these for well under a second (and never across the 11 # post-switch verification delay), so the reference's 3 s touch thread has 12 # nothing to keep alive and is not run. 13 14 def ActM(-A: Type) -> Type: 15 Act.of(A) 16 17 def ActM.pure(-A: Type, x: A) -> Act.of(A): 18 Act.pure(A, x) 19 20 def ActM.bind(-A: Type, -B: Type, m: Act.of(A), f: A -> Act.of(B)) -> Act.of(B): 21 Act.bind(A, B, m, f) 22 23 # Modification time of a path in whole seconds. 24 def Dirlock.mtime(+path: String) -> IO(Result<&1, &1, U32 & String, U32>): 25 import "./effs/mtime.c" 26 import "./effs/mtime.js" 27 28 def poll_ms() -> U32: 29 250 30 31 def mkdir_ok(r: Fs.Res(String)) -> Fs.Res(Bool): 32 match r: 33 case Fail{e}: 34 Done{False{}} 35 case Done{out}: 36 Done{True{}} 37 38 # One attempt: mkdir without -p, so it fails while the lock is held. 39 def take_once(+dir: String) -> Act.of(Bool): 40 IO.bind(Fs.Res(String), Fs.Res(Bool), Fs.tool("/bin/mkdir", [dir]), r => IO.pure(Fs.Res(Bool), mkdir_ok(r))) 41 42 def stale_of(m: Result<&1, &1, U32 & String, U32>, +now: U32, +staleness: U32) -> Bool: 43 match m: 44 case Fail{e}: 45 False{} 46 case Done{t}: 47 U32.is_gt(now, (t + staleness : U32)) 48 49 def is_stale(+dir: String, +staleness: U32) -> Act.of(Bool): 50 do ActM: 51 now : U32 <- Act.lift(U32, Clock.now_s()) 52 m : Result<&1, &1, U32 & String, U32> <- Act.lift(Result<&1, &1, U32 & String, U32>, Dirlock.mtime(dir)) 53 return stale_of(m, now, staleness) 54 55 def reclaim(+stale: Bool, +dir: String) -> Act.of(Unit): 56 Act.cond(Unit, stale, Fs.tool_unit("/bin/rmdir", [dir]), Act.pure(Unit, Unit{})) 57 58 def basename(+p: String) -> String: 59 Maybe.default(&2, String, List.last(&2, String, String.split(p, '/')), p) 60 61 def busy_msg(+dir: String) -> String: 62 "Could not acquire " ++ basename(dir) ++ " — Claude Code appears to be refreshing credentials. Retry in a few seconds." 63 64 # Take over a stale lock, wait a beat, try again. 65 def retake(+dir: String, +staleness: U32) -> Act.of(Bool): 66 do ActM: 67 st : Bool <- is_stale(dir, staleness) 68 reclaim(st, dir) 69 Act.lift(Unit, IO.sleep(poll_ms())) 70 take_once(dir) 71 72 # Wait for the lock `fuel` more times, taking over a stale one. 73 def wait(+fuel: Nat, +dir: String, +staleness: U32, +got: Bool) -> Act.of(Unit): 74 match fuel got: 75 case _ True{}: 76 Act.pure(Unit, Unit{}) 77 case 0n False{}: 78 Act.fail(Unit, 4, busy_msg(dir)) 79 case 1n++p False{}: 80 do ActM: 81 again : Bool <- retake(dir, staleness) 82 wait(p, dir, staleness, again) 83 84 def tries(+timeout_ms: U32) -> Nat: 85 U32.to_nat((timeout_ms / poll_ms() : U32)) 86 87 # Take the lock at `dir` (its parent is created), waiting up to `timeout_ms`. 88 def take(+dir: String, +timeout_ms: U32, +staleness: U32) -> Act.of(Unit): 89 do ActM: 90 FsW.mkdir_p(Fs.dirname(dir)) 91 first : Bool <- take_once(dir) 92 wait(tries(timeout_ms), dir, staleness, first) 93 94 def release(+dir: String) -> Act.of(Unit): 95 IO.bind(Fs.Res(Unit), Fs.Res(Unit), Fs.tool_unit("/bin/rmdir", [dir]), r => IO.pure(Fs.Res(Unit), Done{Unit{}})) 96 97 # Hold the directory lock around `act`, released whatever `act` answers. 98 def with_dirlock(-A: Type, +dir: String, +timeout_ms: U32, +staleness: U32, act: Act.of(A)) -> Act.of(A): 99 do ActM: 100 take(dir, timeout_ms, staleness) 101 Act.finally(A, act, release(dir))