/**
 * NegotiatorGate — the lease/voice send gate (D-B) for the Threadline
 * single-negotiator lock (THREADLINE-SINGLE-NEGOTIATOR-SPEC.md, CMT-1362).
 *
 * This is the chokepoint orchestration that the server's `/threadline/relay-send`
 * route calls right after it mints the effective threadId, BEFORE the content is
 * delivered. It does exactly one decision: enforce the lease so only the owning
 * session is the agent's voice (G1). It has NO authority over content MEANING —
 * binding lives only in the existing operator-anchored primitives (G2, D-C).
 *
 * Behavior by config (FD-7):
 *  - enabled:false (default, ships dark) → pure pass-through. No store write, no
 *    log, zero overhead. The fleet sees no behavior change.
 *  - enabled:true + dryRun:true → observe-only. Acquire/record the lease and log
 *    the verdict it WOULD reach, but ALWAYS allow the send.
 *  - enabled:true + dryRun:false → enforce. A non-owner's CONTENT send is
 *    withheld; the gate emits at most one holding-notice to the peer (rate-
 *    limited, FD-3) and the owning session is the only voice.
 *
 * Fail-open (FD / D-B): a lease-store ERROR fails OPEN for the send (it proceeds)
 * — G1 is explicitly NOT enforced during that window — AND raises a HIGH-priority
 * alert (never silent). Safe because prose is inert (G2): the worst case is two
 * of the agent's own sessions briefly both speaking inert prose, never a binding.
 */
import path from 'node:path';
import { resolveSingleNegotiatorConfig, buildHoldingNotice, shouldEmitHoldingNotice, appendNegotiatorLog, } from './NegotiatorLease.js';
/**
 * Evaluate the send gate for one outbound send. Returns the verdict the route
 * acts on. Performs the lease CAS + holding-notice stamp + observability log
 * internally; never throws into the send path (a store error fails open).
 */
export async function evaluateSendGate(threadId, deps) {
    const cfg = resolveSingleNegotiatorConfig(deps.rawConfig, {
        developmentAgent: deps.developmentAgent,
    });
    // Dark ship: pure pass-through, zero overhead.
    if (!cfg.enabled)
        return { decision: 'allow', reason: 'disabled' };
    const store = deps.conversationStore;
    if (!store) {
        // No store wired — cannot enforce; fail open (this is a wiring gap, alert).
        deps.raiseFailOpenAlert?.({ threadId, sessionName: deps.ownerSessionName ?? '(unknown)', reason: 'no-conversation-store' });
        appendNegotiatorLog(deps.logDir, {
            ts: new Date(deps.now?.() ?? Date.now()).toISOString(),
            threadId,
            action: 'fail-open',
            sessionName: deps.ownerSessionName ?? '(unknown)',
            dryRun: cfg.dryRun,
            detail: 'no-conversation-store',
        });
        return { decision: 'allow', reason: 'fail-open' };
    }
    // A send with no resolvable owning session cannot be lease-checked (no identity
    // to own the voice). Treat as fail-open + alert rather than withhold a real send.
    const ownerSessionName = (deps.ownerSessionName ?? '').trim();
    if (!ownerSessionName) {
        deps.raiseFailOpenAlert?.({ threadId, sessionName: '(none)', reason: 'no-owner-session-identity' });
        appendNegotiatorLog(deps.logDir, {
            ts: new Date(deps.now?.() ?? Date.now()).toISOString(),
            threadId,
            action: 'fail-open',
            sessionName: '(none)',
            dryRun: cfg.dryRun,
            detail: 'no-owner-session-identity',
        });
        return { decision: 'allow', reason: 'fail-open' };
    }
    const nowMs = deps.now?.() ?? Date.now();
    let lease;
    try {
        lease = await store.acquireOrRenewLease(threadId, { ownerSessionName, ownerMachineId: deps.machineId }, { ttlMs: cfg.leaseTtlMs, now: nowMs, isOwnerLive: deps.isSessionLive });
    }
    catch (err) {
        // Store/CAS error — FAIL OPEN for the send, raise a HIGH-priority alert.
        const reason = err instanceof Error ? err.message : String(err);
        deps.raiseFailOpenAlert?.({ threadId, sessionName: ownerSessionName, reason });
        appendNegotiatorLog(deps.logDir, {
            ts: new Date(nowMs).toISOString(),
            threadId,
            action: /CAS retry budget/.test(reason) ? 'cas-exhausted' : 'fail-open',
            sessionName: ownerSessionName,
            dryRun: cfg.dryRun,
            detail: reason,
        });
        return { decision: 'allow', reason: 'fail-open' };
    }
    if (lease.ownedByCaller) {
        appendNegotiatorLog(deps.logDir, {
            ts: new Date(nowMs).toISOString(),
            threadId,
            action: 'allow-own',
            sessionName: ownerSessionName,
            epoch: lease.lease.epoch,
            dryRun: cfg.dryRun,
        });
        return { decision: 'allow', reason: 'own' };
    }
    // Foreign live lease — the caller is NOT the voice.
    if (cfg.dryRun) {
        // Observe-only: log what we WOULD do, still send.
        appendNegotiatorLog(deps.logDir, {
            ts: new Date(nowMs).toISOString(),
            threadId,
            action: 'would-hold',
            sessionName: ownerSessionName,
            ownerSessionName: lease.lease.ownerSessionName,
            epoch: lease.lease.epoch,
            dryRun: true,
        });
        return { decision: 'allow', reason: 'would-hold' };
    }
    // Enforce: WITHHOLD the content. Decide whether to emit a (rate-limited) notice.
    const conv = store.get(threadId);
    const emit = shouldEmitHoldingNotice({
        epoch: lease.lease.epoch,
        lastHoldingNoticeEpoch: conv?.lastHoldingNoticeEpoch,
        lastHoldingNoticeAt: conv?.lastHoldingNoticeAt,
        minIntervalMs: cfg.holdingNoticeMinIntervalMs,
        now: nowMs,
    });
    let notice = null;
    if (emit) {
        notice = buildHoldingNotice(deps.agentName, lease.lease);
        // Stamp the durable rate-limit fields BEFORE the route delivers the notice.
        try {
            await store.recordHoldingNotice(threadId, lease.lease.epoch, nowMs);
        }
        catch {
            // @silent-fallback-ok: a stamp failure at worst allows one extra notice next
            // send; never withhold/deliver differently because the bookkeeping write failed.
        }
    }
    appendNegotiatorLog(deps.logDir, {
        ts: new Date(nowMs).toISOString(),
        threadId,
        action: 'hold',
        sessionName: ownerSessionName,
        ownerSessionName: lease.lease.ownerSessionName,
        epoch: lease.lease.epoch,
        dryRun: false,
        detail: emit ? 'notice-emitted' : 'notice-rate-limited',
    });
    return { decision: 'hold', notice, ownerSessionName: lease.lease.ownerSessionName, epoch: lease.lease.epoch };
}
/** Resolve the standard logs/ directory for negotiator observability. */
export function negotiatorLogDir(stateDir) {
    // stateDir is the agent's .instar dir; logs live as a sibling under it.
    return path.join(stateDir, '..', 'logs');
}
//# sourceMappingURL=NegotiatorGate.js.map