/**
 * 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.
 */
/**
 * 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/<pid>.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 function shouldRunQuietRotation(pidFollow, switched) {
    if (switched)
        return false;
    return pidFollow === 'unavailable';
}
/**
 * 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 function evaluatePidResolverPullback(input) {
    if (input.stalePidStateSessionId === undefined) {
        return { suppress: false, clearStale: false };
    }
    if (input.resolvedCliSessionId === input.stalePidStateSessionId) {
        return { suppress: true, clearStale: false };
    }
    return { suppress: false, clearStale: true };
}
// ─── Two-phase fingerprint-fallback decision ───────────────────────────────
/** 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 const SESSION_ID_FILENAME_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
/** Extract the sessionId portion of a `<sid>.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 function sessionIdFromJsonlPath(path) {
    const base = path.split('/').pop() ?? '';
    return base.endsWith('.jsonl') ? base.slice(0, -'.jsonl'.length) : '';
}
/**
 * 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 function decideFingerprintSwitch(input) {
    const matchedKnown = input.findSubstring((path) => {
        const sid = sessionIdFromJsonlPath(path);
        return SESSION_ID_FILENAME_RE.test(sid) && input.knownSessionIds.has(sid);
    });
    if (matchedKnown) {
        return { action: 'switch', path: matchedKnown, reason: 'known-sid-substring' };
    }
    if (!input.contentNormalized || input.contentNormalized.length === 0) {
        return { action: 'no-match' };
    }
    const exact = input.findExact((path) => {
        const sid = sessionIdFromJsonlPath(path);
        return SESSION_ID_FILENAME_RE.test(sid) && !input.knownSessionIds.has(sid);
    });
    if (exact.length === 0)
        return { action: 'no-match' };
    if (exact.length > 1) {
        return { action: 'abstain', reason: 'multiple-unknown-exact', candidates: exact };
    }
    return { action: 'switch', path: exact[0], reason: 'unknown-sid-exact' };
}
/**
 * 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 `<sessionId>.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 function shouldHealAbsentBaseline(input) {
    if (input.baselineDone)
        return false;
    if (!input.hasJsonlPath)
        return false;
    return !input.jsonlFileExists;
}
//# sourceMappingURL=bridge-rotation-policy.js.map