/**
 * Runtime-side credit halt. The host performs the primary user-facing credit
 * check; this gate is a defense-in-depth read at submit time that refuses only
 * after a cached balance says the user has no credits.
 *
 * The gate is session-scoped and post-run only: updates happen in the
 * background after `agent_end`, the active run is never interrupted, and
 * subagents inherit the parent run's accepted credit decision.
 */
/**
 * Run-scoped credit gate.
 *
 * Lifecycle:
 *   1. Construct once per session.
 *   2. (Optional) Call `update()` at session start to seed the disposition
 *      before the first submit. If you skip this, the first submit will
 *      proceed unconditionally — same as backend where `credit_check_node`
 *      runs in parallel with the first call.
 *   3. On every `agent_end` (success or error), call `update()` to
 *      re-fetch in the background. The next `submit()` will see the
 *      updated disposition.
 *   4. Call `shouldHaltNextSubmit()` synchronously inside `submit()` to read
 *      the disposition. Returns `true` only when the gate has resolved AND
 *      the user has 0 / negative credits.
 *
 * The gate never blocks a submit on a fetch. If `update()` is in flight when
 * `shouldHaltNextSubmit()` is called, the read returns whatever was cached
 * from the previous resolution (or `false` if nothing has resolved yet — the
 * fail-open default).
 */
export class EmbeddedAgentCreditGate {
    deps;
    /** Resolved disposition. `false` until the first fetch completes. */
    resolvedShouldHalt = false;
    /** True once the first fetch has completed (success, failure, or skip). */
    hasResolved = false;
    /**
     * Set when update() is called with a skip flag. Once skipped for a session,
     * all subsequent reads return `false`
     * and `update()` is a no-op. The host re-builds the gate if a session's
     * identity changes (rare; not a normal flow).
     */
    skipped = false;
    /** Tracks the in-flight update so concurrent callers collapse correctly. */
    inFlight;
    constructor(deps) {
        this.deps = deps;
        if (deps.initialCreditBalance) {
            this.resolvedShouldHalt = !deps.initialCreditBalance.hasCredits;
            this.hasResolved = true;
        }
    }
    /**
     * Background-update the cached disposition. At session start, callers pass
     * skip flags here; after run boundaries, callers usually omit options.
     * Idempotent: if a fetch is already in flight, returns the existing promise.
     */
    update(options = {}) {
        const hasSkipDecision = 'isServiceAccount' in options ||
            'isBenchmark' in options ||
            'skipModelCreditGate' in options ||
            'isByok' in options;
        const shouldSkip = Boolean(options.isServiceAccount ||
            options.isBenchmark ||
            options.skipModelCreditGate ||
            options.isByok);
        if (shouldSkip) {
            this.skipped = true;
            this.resolvedShouldHalt = false;
            this.hasResolved = true;
            return Promise.resolve();
        }
        if (hasSkipDecision)
            this.skipped = false;
        if (this.skipped) {
            return Promise.resolve();
        }
        if (this.inFlight)
            return this.inFlight;
        this.inFlight = this._runUpdate().finally(() => {
            this.inFlight = undefined;
        });
        return this.inFlight;
    }
    async _runUpdate() {
        const credit = await this.deps.fetchCreditBalance();
        // Fail open: a fetch failure must not block a legitimate user.
        const hasCredits = credit?.hasCredits ?? true;
        this.resolvedShouldHalt = !hasCredits;
        this.hasResolved = true;
    }
    /**
     * Synchronous read for `submit()` — returns `true` if the upcoming submit
     * should be refused (user is out of credits), `false` otherwise.
     *
     * Pure read of resolved state. Never blocks. Never throws.
     */
    shouldHaltNextSubmit() {
        if (this.skipped)
            return false;
        if (!this.hasResolved)
            return false;
        return this.resolvedShouldHalt;
    }
    /** Test/debug accessor — true iff the first fetch has completed. */
    get isResolved() {
        return this.hasResolved;
    }
    /**
     * Wait for any in-flight update to complete. Tests call this to
     * deterministically observe the resolved disposition. Production code
     * should not need this.
     */
    whenIdle() {
        return this.inFlight ?? Promise.resolve();
    }
}
//# sourceMappingURL=credit-gate.js.map