/**
 * Incremental projection of durable agent inbox events.
 *
 * @module @deepseek-ai/dsh-agent/inbox
 */
/** A replay-once projection that incrementally consumes later inbox splices. */
export class Inbox {
    session;
    notifications;
    state = { 'next-turn': [], 'next-step': [] };
    constructor(session, notifications) {
        this.session = session;
        this.notifications = notifications;
        for (const event of session.events.slice(session.header.seedLength ?? 0)) {
            if (event.type !== 'agent/inbox/spliced')
                continue;
            try {
                this.apply(event.data);
            }
            catch (error) {
                throw new Error(`invalid persisted inbox splice at session seq ${event.seq}`, { cause: error });
            }
        }
    }
    /** Prompts awaiting individual turns. */
    get nextTurn() {
        return this.state['next-turn'];
    }
    /** Input awaiting the next step boundary. */
    get nextStep() {
        return this.state['next-step'];
    }
    /** Whether either pending-message list contains work. */
    get hasPending() {
        return this.nextTurn.length > 0 || this.nextStep.length > 0;
    }
    /** Durably cancel all pending input, clearing next-step before next-turn. */
    clear() {
        this.splice('next-step', 0, this.nextStep.length, []);
        this.splice('next-turn', 0, this.nextTurn.length, []);
    }
    /**
     * Remove and return the complete batch proposed for one step, publishing
     * each claimed message. The durable splices are pure deletions.
     * @param target - whether this boundary also consumes one queued turn.
     * @param turn - turn that will own the claimed batch.
     * @returns next-step input followed by the queued turn, when requested.
     * @internal - The agent loop's step-boundary operation, not a plugin extension point.
     */
    claim(target, turn) {
        const claimed = this.mutate('next-step', 0, this.nextStep.length, [], false);
        if (target === 'next-turn') {
            claimed.push(...this.mutate('next-turn', 0, 1, [], false));
        }
        for (const message of claimed)
            this.notifications.claimed(message, turn);
        return claimed;
    }
    /**
     * Append one message to a pending list and durably record the insertion.
     * @param target - pending list to extend.
     * @param message - message to append.
     * @throws if the message identity is already pending.
     */
    append(target, message) {
        this.splice(target, this.state[target].length, 0, [message]);
    }
    /**
     * Prepend one message to a pending list and durably record the insertion.
     * @param target - pending list to extend.
     * @param message - message to prepend.
     * @throws if the message identity is already pending.
     */
    prepend(target, message) {
        this.splice(target, 0, 0, [message]);
    }
    /**
     * Replace one pending message in place, possibly changing its identity. A
     * successful replacement publishes the old message as discarded and the new
     * message as inserted.
     * @param messageId - identity of the pending message to replace.
     * @param newMessage - replacement message.
     * @returns whether the message was still pending.
     * @throws if the replacement duplicates another pending message identity.
     */
    replace(messageId, newMessage) {
        const location = this.locate(messageId);
        if (location === undefined)
            return false;
        this.splice(location.target, location.index, 1, [newMessage]);
        return true;
    }
    /**
     * Remove one pending message and durably record its cancellation.
     * @param messageId - identity of the pending message to remove.
     * @returns whether the message was still pending.
     */
    remove(messageId) {
        const location = this.locate(messageId);
        if (location === undefined)
            return false;
        this.splice(location.target, location.index, 1, []);
        return true;
    }
    /**
     * Apply standard splice semantics and durably record the normalized result.
     * The durable event commits before the live projection mutates, so synchronous
     * `session/event` observers see the pre-splice lists and can reconstruct the
     * removed messages from the normalized coordinates.
     * @param target - pending list to mutate.
     * @param start - splice position.
     * @param deleteCount - maximum number of messages to remove.
     * @param inserted - messages to insert at the resolved position.
     * @returns messages removed by the splice.
     */
    splice(target, start, deleteCount, inserted) {
        return this.mutate(target, start, deleteCount, inserted, true);
    }
    /** Locate one pending identity across both owned lists. */
    locate(messageId) {
        for (const target of ['next-turn', 'next-step']) {
            const index = this.state[target].findIndex(message => message.id === messageId);
            if (index >= 0)
                return { target, index };
        }
        return undefined;
    }
    /** Commit one normalized mutation and publish its live notifications. */
    mutate(target, start, deleteCount, inserted, discardRemoved) {
        const inbox = this.state[target];
        const truncatedStart = Math.trunc(start);
        const offset = Number.isNaN(truncatedStart) ? 0 : truncatedStart;
        const actualStart = offset < 0
            ? Math.max(inbox.length + offset, 0)
            : Math.min(offset, inbox.length);
        const truncatedDeleteCount = Math.trunc(deleteCount);
        const actualDeleteCount = Math.min(Math.max(Number.isNaN(truncatedDeleteCount) ? 0 : truncatedDeleteCount, 0), inbox.length - actualStart);
        if (actualDeleteCount === 0 && inserted.length === 0)
            return [];
        const outcome = discardRemoved && actualDeleteCount > 0 ? 'canceled' : undefined;
        const splice = {
            target,
            start: actualStart,
            ...(actualDeleteCount === 0 ? {} : { removedCount: actualDeleteCount }),
            inserted,
            ...(outcome === undefined ? {} : { outcome }),
        };
        this.validate(splice);
        const event = this.session.append('agent/inbox/spliced', splice);
        const removed = inbox.splice(actualStart, actualDeleteCount, ...event.data.inserted);
        if (discardRemoved) {
            for (const message of removed)
                this.notifications.discarded(message);
        }
        for (const message of event.data.inserted)
            this.notifications.inserted(message);
        return removed;
    }
    /** Apply one normalized durable splice to the projection. */
    apply(splice) {
        this.validate(splice);
        const inbox = this.state[splice.target];
        return inbox.splice(splice.start, splice.removedCount ?? 0, ...splice.inserted);
    }
    /** Validate one normalized splice against the current projection. */
    validate(splice) {
        const inbox = this.state[splice.target];
        const removedCount = splice.removedCount ?? 0;
        if (!Number.isSafeInteger(splice.start) || splice.start < 0 || splice.start > inbox.length
            || !Number.isSafeInteger(removedCount) || removedCount < 0
            || splice.start + removedCount > inbox.length) {
            throw new Error('invalid inbox splice');
        }
        const candidate = inbox.toSpliced(splice.start, removedCount, ...splice.inserted);
        const ids = new Set();
        for (const message of splice.target === 'next-turn'
            ? [...candidate, ...this.nextStep]
            : [...this.nextTurn, ...candidate]) {
            if (ids.has(message.id))
                throw new Error(`message "${message.id}" is already pending`);
            ids.add(message.id);
        }
    }
}
//# sourceMappingURL=inbox.js.map