/** * The ownership lease: which pi session may run a poller for this account. * * One iLink account supports one poller. Two pi processes — two terminals, * two projects — each load this extension against the *same* SDK credential * directory and the *same* `cursor.json`, so the second one long-polls the * same account and each incoming message lands wherever the cursor was * advanced first. The owner's conversation is then split across two sessions * with different contexts, answering at random. That is the failure this * module exists to make impossible. * * The token is the state file's `pid` plus a renewed `savedAt`. `state.ts` * already decided what it *means* (`shouldAdoptState`, `claimHeldBy`) and * those decisions were testable; the protocol around them — write the claim * before polling, read it back, re-read before every renewal, retry while * blocked — lived as five functions and three timers inside one closure, and * could only be asserted end to end by a test that was not allowed to exist * (no smoke test may let a worker spawn). Here it is a state machine over an * injected clock and an injected file, so the two directions of `--takeover` * can both be driven directly. * * What stays outside: * * The mirror. The state file carries the queue, the dedup set and an * undelivered reply as well as the claim; serialising it belongs to whoever * owns those. This module only knows *when* the claim must be on disk, and * says so through `io.persistNow()`. * * The worker. `onSeized` must stop it and `onRefused` must not (there was * never one), but neither is this module's to touch. * * There is no `release()`. Nothing in this channel gives ownership up * voluntarily — `/wechat stop` keeps it, and `retireInstance()` deliberately * leaves the mirror on disk under its own pid so the next session inherits * the queue by letting the lease lapse. A method with no caller would only * suggest a path that does not exist. */ import type { Logger } from '../log.js'; import { type ParsedState } from '../state.js'; import { type ChannelState, type Owns } from './core.js'; import type { Timers } from './timers.js'; export declare const VERIFY_DELAY_MIN_MS = 400; export declare const VERIFY_DELAY_SPAN_MS = 600; export interface LeaseIo { selfPid: number; /** The current mirror, or null when there is no file or it will not parse. */ readState(): ParsedState | null; isProcessAlive(pid: number): boolean; /** Put the mirror — claim included — on disk now. */ persistNow(): void; } export interface LeaseEvents { /** * The channel is ours. `parsed` is the file we won it from (null on a * fresh install), handed over once so the caller can restore the queue, * the dedup set and any undelivered reply without reading the file again. * * Fired only on the blocked→owned edge. That is what turns "restore * exactly once per acquisition" from a property of who happened to call * whom into a property of this module. */ onClaimed(parsed: ParsedState | null, ctx: { via: 'initial' | 'retry' | 'takeover'; }): void; /** Never had it: somebody else held it when we asked. No worker to stop. */ onRefused(info: { reason: string; holderPid: number; }): void; /** * Had it, lost it. The worker **must** stop here: a seized session that * keeps polling is exactly the crosstalk the lease exists to prevent, and * this time we would be causing it ourselves. */ onSeized(info: { reason: string; holderPid: number; }): void; } export interface LeaseDeps { /** The only module that may write `ownership`. */ S: Owns; io: LeaseIo; timers: Timers; events: LeaseEvents; log: Logger; } export interface Lease { /** * Ask for the channel: `session_start`, and `/wechat start --takeover` * with `force`. A no-op while we already hold it — owning *is* the record * of having restored, and a second restore would replay answered messages. */ claim(opts?: { force?: boolean; }): void; /** * Re-read the lease wherever crosstalk would become visible: before a * renewal, on an incoming message, at the end of a turn, before dispatch. * Returns true when this session no longer owns the channel. */ check(where: string): boolean; /** Retirement: stop this module's timers. Writes nothing, fires nothing. */ stop(): void; } export declare function createLease({ S, io, timers, events, log }: LeaseDeps): Lease; //# sourceMappingURL=lease.d.ts.map