/**
 * GAP-B commitment-evidence backstop — the pure qualifying + agreement predicate.
 *
 * Spec: docs/specs/autonomous-registration-guarantee.md (Part B + Frontloaded
 * Decisions D1/D2/D7/D8).
 *
 * The reaper revives a REGISTERED autonomous run from its per-topic state file.
 * An UNregistered-but-actively-working run (operator said "go autonomous" but the
 * skill never wrote the state file) has no state file → today it dies at the
 * age-limit reap. This module is the BACKSTOP: it recognizes an independent
 * live-work signal — a FRESH open agent-commitment for the topic, CORROBORATED by
 * a recent user message — and (when armed, dark by default) lets the existing
 * revival machinery keep the run alive.
 *
 * This file holds ONLY the deterministic predicates (D1 freshness, D2 qualifying
 * set, D8 agreement). The wiring (the dark-gate, the injection, the Part A
 * surface, the drain-time re-check) lives at the server callsite. Extracting the
 * predicate here makes D1/D2/D7/D8 unit-testable against a REAL CommitmentTracker
 * with no server boot — and guarantees the enqueue-time check and the drain-time
 * re-check (D9) compute the IDENTICAL verdict (they call this same function), so
 * they can never disagree.
 *
 * Signal vs Authority: this returns a boolean SIGNAL. The authority that decides
 * revival remains the unchanged `evidenceEligible`/drainer. No new WorkEvidence
 * enum value; provenance rides the reason tag + the reap-log `evidenceSource`.
 */
export function recentUserMessageFromHistory(history, withinMs, now = Date.now()) {
    const cutoff = now - withinMs;
    for (let i = history.length - 1; i >= 0; i--) {
        const entry = history[i];
        if (!entry.fromUser)
            continue; // skip agent/system echoes
        const ts = Date.parse(entry.timestamp);
        if (!Number.isFinite(ts))
            continue;
        // The first USER entry scanning backwards is the NEWEST user message — its
        // recency is the verdict (every earlier user entry is older).
        return ts >= cutoff;
    }
    return false;
}
/**
 * D2 qualifying-commitment predicate (the per-commitment half).
 *
 * A commitment counts as live-work evidence only if ALL hold:
 *  - `status === 'pending'` (NOT verified/violated — a violated commitment is a
 *    FAILING session, not a working one);
 *  - agent-driven: `owner === 'agent'` (default), OR `owner === 'user'` with
 *    `blockedOn` ∈ {none, undefined} — a commitment WAITING on the user
 *    (`blockedOn` ∈ {user-input, user-authorization}) is the opposite of an
 *    active autonomous run and is excluded;
 *  - not beacon-paused and not beacon-suppressed;
 *  - local-origin: `originMachineId` is this machine or absent (a replicated peer
 *    commitment is advisory data, never revival authority);
 *  - D1 freshness: `createdAt` within `freshCommitmentWindowMs` — `createdAt` ONLY
 *    (no `updatedAt` exists; bookkeeping/beacon timestamps must NOT refresh
 *    freshness — a 3-day promise a beacon pinged 5min ago is not a live session).
 */
export function commitmentQualifies(c, deps) {
    if (c.status !== 'pending')
        return false;
    const owner = c.owner ?? 'agent';
    const blockedOn = c.blockedOn ?? 'none';
    const agentDriven = owner === 'agent' || (owner === 'user' && blockedOn === 'none');
    if (!agentDriven)
        return false;
    if (c.beaconPaused || c.beaconSuppressed)
        return false;
    if (c.originMachineId && deps.ownMachineId && c.originMachineId !== deps.ownMachineId) {
        return false;
    }
    const nowMs = (deps.now ?? Date.now)();
    const createdMs = Date.parse(c.createdAt);
    if (!Number.isFinite(createdMs) || createdMs < nowMs - deps.freshCommitmentWindowMs) {
        return false;
    }
    return true;
}
/**
 * The full GAP-B eligibility verdict for a topic (D1/D2 + D8 agreement).
 *
 * True iff (a) at least one qualifying commitment exists for the topic (D1/D2),
 * AND (b) there is a recent user message within the stale-commitment window (D8 —
 * the SAME corroboration ReapGuard's commitment KEEP uses, so KEEP and
 * eligibility AGREE; without this the 2026-06-13 13-session reap/revive loop
 * returns). Fail-open (D7): any read throwing ⇒ false (no injection), never a
 * spurious eligibility.
 */
export function gapBEligibleForTopic(topicId, source, deps) {
    try {
        const qualifies = source
            .getActiveByTopicId(topicId)
            .some((c) => commitmentQualifies(c, deps));
        if (!qualifies)
            return false;
        // D8 AGREEMENT — non-negotiable. A commitment that would NOT keep the session
        // alive (no recent user message) cannot revive it either.
        return deps.recentUserMessage(topicId, deps.staleCommitmentWindowMs);
    }
    catch {
        // @silent-fallback-ok — D7: a throwing tracker/predicate contributes NOTHING;
        // the reap proceeds exactly as today (no injection, no revive).
        return false;
    }
}
/**
 * Resolve the Part B dark-gate from the resumeQueue config block (D5).
 * `enabled` MUST be an explicit `true` — absence/false ⇒ disarmed (no injection
 * on fleet OR dev). `dryRun` defaults true when armed.
 */
export function resolveGapBInjectionGate(cfg) {
    const armed = cfg?.enabled === true;
    return { armed, dryRun: cfg?.dryRun !== false };
}
/**
 * Compute the reaped-session GAP-B decision. Pinned to the SAME branch the
 * state-file source uses: an `age-limit` reap whose per-topic state file is
 * ABSENT (`stateFilePresent === false` — a registered run is handled upstream by
 * AGE_LIMIT_ACTIVE_RUN_REASON). When disarmed, returns a strict no-op
 * `{fired:false, inject:false}` (the dark-default containment: no injection ⇒ no
 * revival ⇒ the 2026-06-13 loop is structurally impossible while dark).
 *
 * `eligible` is the precomputed `gapBEligibleForTopic` verdict (kept a parameter
 * so the caller can short-circuit the cost when disarmed, and so this stays a
 * pure decision function over booleans).
 */
export function decideGapBInjection(input) {
    if (!input.gate.armed)
        return { fired: false, inject: false };
    if (input.reason !== 'age-limit')
        return { fired: false, inject: false };
    if (input.stateFilePresent)
        return { fired: false, inject: false };
    if (!input.eligible)
        return { fired: false, inject: false };
    return { fired: true, inject: !input.gate.dryRun };
}
//# sourceMappingURL=gapBCommitmentEvidence.js.map