import type { PathCtx } from '../config/paths.js'; /** * One login per account, however many copies exist. * * A refresh token is single-use: renewing rotates it, and every other copy of * the old one is dead from that moment. With one directory per session, an * account's login exists in several places at once (the profile, plus every * live session running that account), and each copy renews on its own clock. * Whoever renewed first killed the rest: the operator's sessions took turns * hitting "please run /login", and the profile itself was often the stalest * copy, so every NEW session started on a corpse. Found live: three copies of * one lineage across two sessions and the profile, a fourth session holding an * already-retired lineage, and four dead lineages in the refusal log. * * The model this file enforces: the PROFILE is the hub. A session that renews * pushes its login home (the mirror in session.ts, which asks the API who the * login belongs to before writing). This file adds the missing direction: * * - PULL: when the hub holds a NEWER lineage than a running session, the * session's copy is replaced before its Claude tries to refresh the retired * one. Claude re-reads the file under its own lock before refreshing (that is * how several plain `claude` terminals survive sharing one file), so a copy * swapped in between refreshes is picked up cleanly. * - RECOVER: when the hub's login is dead but a live session of the SAME * account holds a working one, adopt it instead of telling the operator to * sign in. The session's copy was renewed by its Claude; the hub just never * heard. * * Identity is the hard rule in both directions: nothing is copied between a * session and a profile unless they are the same registered account. A session * that became somebody else via /login keeps its own login untouched; rotation * realigns it, not sync. */ export interface SyncAccount { name: string; dir: string; /** The address recorded at registration; the identity rule needs it. */ email?: string; } export type PullDecision = { pull: true; reason: string; } | { pull: false; reason: string; }; export interface PullEvidence { /** Is the session's copy a usable login at all? */ sessionUsable: boolean; /** Is the profile's? */ profileUsable: boolean; /** Same refresh-token lineage on both sides? */ sameLineage: boolean; /** Who the session is signed in as, when recorded. */ sessionEmail: string | null; /** The account's registered address, when recorded. */ accountEmail: string | null; /** When each side's access token expires; 0 when unknown. */ sessionExpiresAt: number; profileExpiresAt: number; } /** * Should the profile's login replace the session's copy? * * Pure, so every branch is testable. The expensive mistakes each get a rule: * overwriting a live session that became a different account would hijack a * running Claude (identity rule); overwriting a session whose copy is NEWER * would un-renew it (freshness rule); doing nothing when the session's lineage * is retired is the /login bug this file exists to end. */ export declare function decidePull(e: PullEvidence): PullDecision; export type PullResult = 'pulled' | 'skipped' | 'busy'; /** * Carry a renewal that happened elsewhere INTO this running session. * * Runs under the same lock Claude uses to coordinate its refreshes, and only * when that lock is free: a busy lock means a refresh is mid-write, which is * exactly when to come back on the next tick instead. */ export declare function pullProfileIntoSession(account: SyncAccount, sessionDir: string, ctx?: PathCtx): PullResult; export interface RecoverResult { recovered: boolean; /** Which session's login was adopted, for the log. */ fromPid?: number; } /** * A profile whose login is dead, while a live session of the same account runs * on a working one: adopt the session's copy instead of demanding a sign-in. * * Only on positive identity: the lease says which account the session is FOR, * but leases have lied before (a contaminated session carried another account's * login under the right lease), so the session's own recorded identity must * match the account's registered address. No address, no adoption. */ export declare function recoverLoginFromLiveSession(account: SyncAccount, ownSessionDir: string | null, ctx?: PathCtx): RecoverResult;