/** * Pure rotation-policy gate for the Claude bridge watcher. * * Lives outside `worker.ts` so tests can import without dragging worker-level * fs / IPC side-effects. */ export type PidFollowResult = 'unavailable' | 'same' | 'switched'; /** * Decide whether `bridgeIngest` should fall through to the directory-mtime * `maybeFollowQuietRotation` heuristic this tick. * * Inputs: * - `pidFollow`: result of the authoritative pid-state probe * (`maybeFollowSessionRotationViaPid`). `'switched'` means it already * moved us; `'same'` means the pid file's `sessionId` matches our * current path; `'unavailable'` means the pid file was unreadable * (non-Linux, no `~/.claude/sessions/.json`, validation failure). * - `switched`: whether ANY earlier rotation step (pid resolver OR * fingerprint fallback) already moved the watcher this tick. * * Returns true only when there was no earlier switch AND the pid resolver * gave no opinion. * * Trade-off: pid resolver `'same'` is NOT proof that no rotation happened * — Claude Code 2.1.123 writes `sessionId` ONCE at process start and the * in-pane `/clear` path does not refresh it. We still skip the mtime * heuristic on `'same'` because the alternative is sibling-pane hijack: * any other Claude pane in the same cwd gets a busier jsonl and the * heuristic picks it. The cost is that a pure-local `/clear` with no * pending Lark turn won't auto-follow until the user sends a Lark * message (which arms fingerprint fallback). The Lark-message path is * the dominant /clear recovery flow in practice; sibling-pane * corruption would silently break every multi-pane adopt setup. * * `--resume` is a fresh spawn and rewrites the pid file's sessionId, so * it surfaces here as `'switched'`, not `'same'` — it's not affected by * this gate. */ export declare function shouldRunQuietRotation(pidFollow: PidFollowResult, switched: boolean): boolean; export interface PidResolverPullbackInput { /** sessionId reported by the pid file this tick. */ resolvedCliSessionId: string; /** The pid file's full jsonl path (derived from sessionId + cwd). */ resolvedPath: string; /** Current bridge jsonl path. */ currentBridgeJsonlPath: string | undefined; /** Sid recorded as "stale" by the fingerprint fallback the last time it * accepted a candidate the pid file disagreed about. Undefined when no * fingerprint accept has overridden the pid file. */ stalePidStateSessionId: string | undefined; } export interface PidResolverPullbackDecision { /** True ⇒ pid resolver should report 'same' rather than rotate the * watcher back to `resolvedPath`. */ suppress: boolean; /** True ⇒ caller should clear `stalePidStateSessionId`: a fresh sid * (different from the stale one) appeared in the pid file, meaning a * real rotation has happened (`--resume` / fresh spawn) and the prior * fingerprint accept is no longer relevant. */ clearStale: boolean; } /** * Decide whether the pid resolver should pull the watcher back to a path * that disagrees with the current bridgeJsonlPath, given the worker's * staleness bookkeeping. * * Rules: * - No `stalePidStateSessionId` recorded ⇒ honour pid resolver as before. * - Recorded sid matches pid file's current sid ⇒ suppress: this is the * spawn-time sid that fingerprint fallback already overrode for an * in-pane /clear that pid file can't see. * - Recorded sid differs from pid file's current sid ⇒ a NEW rotation has * happened (`--resume` / fresh spawn / Claude restart with new pid file * contents). Clear the stale flag and let pid resolver switch normally. */ export declare function evaluatePidResolverPullback(input: PidResolverPullbackInput): PidResolverPullbackDecision; /** UUID-shaped Claude jsonl filename (sessionId.jsonl). Duplicated from * `src/adapters/cli/claude-code.ts:SESSION_UUID_RE` to keep this module * free of adapter imports. Keep the two patterns in sync; both gate * trust-set membership and fingerprint-fallback candidate eligibility. */ export declare const SESSION_ID_FILENAME_RE: RegExp; /** Extract the sessionId portion of a `.jsonl` path (basename minus * the `.jsonl` extension). Returns the empty string when the path lacks * the expected suffix; callers should treat that as "untrusted". */ export declare function sessionIdFromJsonlPath(path: string): string; export interface FingerprintSwitchInput { /** Substring fingerprint to feed Phase 1's scanner. Required. */ contentFingerprint: string; /** Full normalised content of the Lark turn. Phase 2 (unknown-sid * exact-content recovery) is skipped when this is missing or empty — * short content can't anchor a recovery without ambiguity. */ contentNormalized?: string; /** Trust set populated from initial attach, pid resolver hits, fd probes. */ knownSessionIds: ReadonlySet; /** Phase 1 scanner: substring fingerprint search. Caller wires it to * `findJsonlContainingFingerprint`; this helper only sees the result. */ findSubstring: (acceptCandidate: (path: string) => boolean) => string | null; /** Phase 2 scanner: exact-content search returning ALL matches in * mtime-descending order. Caller wires it to * `findJsonlsContainingExactContent`. */ findExact: (acceptCandidate: (path: string) => boolean) => string[]; } export type FingerprintSwitchDecision = { action: 'switch'; path: string; reason: 'known-sid-substring' | 'unknown-sid-exact'; } | { action: 'abstain'; reason: 'multiple-unknown-exact'; candidates: string[]; } | { action: 'no-match'; }; /** * Two-phase candidate selection for the bridge fingerprint fallback. * * Phase 1 (known-sid substring): the cheap path. Run the substring * fingerprint scanner with an acceptCandidate predicate that requires * `(UUID-shaped sid) AND (sid in knownSessionIds)`. Sibling panes are * UUID-shaped but not in our trust set — their fingerprint hits are * rejected. UUID gate also blocks accidental non-Claude jsonls. * * Phase 2 (unknown-sid exact-content recovery): only runs when * `contentNormalized` is non-empty. Fires when Phase 1 found no match, * which is the worst-case in-pane `/clear` scenario where the new sid * never reaches our trust set (pid file lags, fd probe missed the open * window). Run the exact-content scanner with predicate * `(UUID-shaped sid) AND (sid NOT in knownSessionIds)`. Decision: * - 0 matches → no-match * - 1 match → switch (the post-/clear file we couldn't otherwise see) * - ≥2 matches → abstain; multiple untrusted files normalise to the * same Lark content, so we cannot pick one without further evidence. * Caller is expected to log and surface a diagnostic. * * Pure: never touches fs / IPC / module state. Tests inject fakes for * `findSubstring` and `findExact` to exercise every branch. */ export declare function decideFingerprintSwitch(input: FingerprintSwitchInput): FingerprintSwitchDecision; export interface AbsentBaselineHealInput { /** Whether the bridge has already completed baseline (gate already open). */ baselineDone: boolean; /** Whether a `bridgeJsonlPath` is currently pinned. */ hasJsonlPath: boolean; /** Whether that pinned path currently exists on disk. */ jsonlFileExists: boolean; } /** * Decide whether a pending Lark turn arriving against a not-yet-baselined * bridge should SELF-HEAL by arming fresh-empty readiness, instead of being * dropped with "baseline not ready". * * Background: in `baseline-existing` (resume / restart-restore) mode the * worker pins `bridgeJsonlPath` to `.jsonl` and waits for that * file to appear before baselining. If Claude wrote its transcript under a * DIFFERENT sessionId than the one we guessed — `--session-id` not honoured, * a stale resume id, or an /adopt sid persisted as the botmux sid — the * guessed file never appears, `baselineDone` stays false forever, and every * turn is dropped ("baseline not ready"). The bridge is then permanently * stuck on a path that does not exist (the user-reported "message went into * the PTY but final_output was never attributed" symptom). The pid-state * resolver that would normally correct this needs a live CLI pid, which is * often missing on restart-restore — so the only recovery path that still * works is the exact-content fingerprint scan, and IT is starved because no * turn is ever marked. * * An ABSENT file has no prior history to absorb, so it is safe to declare * fresh-empty readiness (offset 0, baseline done) here: the pending turn * then gets marked, which arms the per-tick exact-content fingerprint * recovery (`decideFingerprintSwitch`) to discover the jsonl Claude actually * appended this message to and switch the bridge onto it — no dependence on * Claude's internal pid-state / tasks files, so it is Claude-version-robust. * * Returns true ONLY when baseline isn't done, a path is pinned, and that * path is absent. When the file EXISTS (the genuine slow-resume-with-history * case), this returns false so normal lazy-baseline runs instead and prior * turns are absorbed rather than re-emitted. */ export declare function shouldHealAbsentBaseline(input: AbsentBaselineHealInput): boolean; //# sourceMappingURL=bridge-rotation-policy.d.ts.map