import { RelayCast } from '@relaycast/sdk';
import { relaycastTelemetryOptions } from '../relaycast-telemetry.js';
import { normalizeAgent, normalizeAgentPresence, normalizeAgentRegistration, normalizeChannel, normalizeChannelMember, normalizeChannelName, normalizeChannelReadStatus, normalizeDeliveryTransition, normalizeGroupDirectConversation, normalizeInbox, normalizeInboxItem, normalizeMessage, normalizeMessagingEvent, normalizeReaction, normalizeReadReceipt, normalizeSearchResult, normalizeThread, } from './normalize.js';
/**
 * Translate a relay capability registration into a relaycast `actions.register`
 * request. Relaycast 2.x replaced the `commands` registry with `actions`:
 * `command` → `name`, `parameters` → `inputSchema`.
 */
function toRegisterActionRequest(input) {
    // `inputSchema` (a converted JSON Schema) takes precedence over the legacy
    // `parameters` field when both are present.
    const inputSchema = input.inputSchema ?? input.parameters;
    return {
        name: input.command,
        description: input.description,
        handlerAgent: input.handlerAgent,
        ...(inputSchema === undefined ? {} : { inputSchema }),
        ...(input.outputSchema === undefined ? {} : { outputSchema: input.outputSchema }),
        ...(input.availableTo === undefined ? {} : { availableTo: input.availableTo }),
    };
}
/**
 * Translate a relaycast `ActionDefinition` back into a relay `RelayCapability`,
 * preserving the relay-facing `command`/`parameters` vocabulary.
 */
function toRelayCapability(raw) {
    const action = (raw ?? {});
    const command = (action.name ?? action.command);
    return {
        ...action,
        command,
        description: action.description,
        handlerAgent: action.handlerAgent,
        parameters: action.inputSchema ?? action.parameters,
    };
}
function toRelayNode(raw) {
    const node = (raw ?? {});
    const rawStatus = readStr(node, 'status');
    return {
        id: readStr(node, 'id', 'node_id'),
        nodeId: readStr(node, 'nodeId', 'node_id'),
        name: readStr(node, 'name') ?? '',
        status: rawStatus === 'online' || rawStatus === 'offline' ? rawStatus : 'unknown',
        live: readBoolean(node, 'live'),
        capabilities: Array.isArray(node.capabilities) ? node.capabilities.map(toRelayNodeCapability) : [],
        repoKeys: readRepoKeys(node),
        maxAgents: readNumber(node, 'maxAgents', 'max_agents'),
        activeAgents: readNumber(node, 'activeAgents', 'active_agents'),
        handlersLive: readBoolean(node, 'handlersLive', 'handlers_live'),
        load: readNumber(node, 'load'),
        lastHeartbeatAt: readStr(node, 'lastHeartbeatAt', 'last_heartbeat_at'),
        createdAt: readStr(node, 'createdAt', 'created_at'),
        tags: readStringArray(node, 'tags'),
        version: readStr(node, 'version'),
    };
}
function readRepoKeys(node) {
    const direct = readStringArray(node, 'repoKeys') ?? readStringArray(node, 'repo_keys');
    if (direct)
        return direct;
    const repoPaths = readRecord(node, 'repoPaths', 'repo_paths');
    return repoPaths ? Object.keys(repoPaths).filter(Boolean) : undefined;
}
function toRelayNodeCapability(raw) {
    const capability = (raw ?? {});
    return {
        name: readStr(capability, 'name') ?? '',
        kind: readStr(capability, 'kind'),
        metadata: readRecord(capability, 'metadata'),
    };
}
function toRelayTrigger(raw) {
    const trigger = (raw ?? {});
    return {
        id: readStr(trigger, 'id'),
        channel: readStr(trigger, 'channel'),
        pattern: readStr(trigger, 'pattern', 'match'),
        mention: readMention(trigger.mention),
        actionName: readStr(trigger, 'actionName', 'action_name') ?? '',
        enabled: readBoolean(trigger, 'enabled') ?? true,
    };
}
function toTriggerRequest(input) {
    return {
        ...(input.channel !== undefined ? { channel: input.channel } : {}),
        ...(input.pattern !== undefined ? { pattern: input.pattern } : {}),
        ...(input.mention !== undefined ? { mention: input.mention } : {}),
        ...(input.actionName !== undefined
            ? { actionName: input.actionName, action_name: input.actionName }
            : {}),
        ...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
    };
}
function readNumber(record, ...keys) {
    for (const key of keys) {
        const value = record[key];
        if (typeof value === 'number' && Number.isFinite(value))
            return value;
    }
    return undefined;
}
function readBoolean(record, ...keys) {
    for (const key of keys) {
        const value = record[key];
        if (typeof value === 'boolean')
            return value;
    }
    return undefined;
}
function readStringArray(record, key) {
    const value = record[key];
    return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : undefined;
}
function readMention(value) {
    return typeof value === 'boolean' || typeof value === 'string' ? value : undefined;
}
/** Translate a relay completion result into the relaycast `CompleteInvocationRequest` shape. */
function toCompleteInvocationRequest(data) {
    return {
        ...(data.output === undefined ? {} : { output: data.output }),
        ...(data.error === undefined ? {} : { error: data.error }),
        ...(data.durationMs === undefined ? {} : { durationMs: data.durationMs }),
    };
}
function asRecord(value) {
    return value !== null && typeof value === 'object' && !Array.isArray(value)
        ? value
        : {};
}
function readStr(record, ...keys) {
    for (const key of keys) {
        const value = record[key];
        if (typeof value === 'string')
            return value;
    }
    return undefined;
}
function readRecord(record, ...keys) {
    for (const key of keys) {
        const value = record[key];
        if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
            return value;
        }
    }
    return undefined;
}
export class RelayPlacementError extends Error {
    code;
    capability;
    node;
    repo;
    attempts;
    constructor(code, message, context) {
        super(message);
        this.name = 'RelayPlacementError';
        this.code = code;
        this.capability = context.capability;
        this.node = context.node;
        this.repo = context.repo;
        this.attempts = context.attempts;
    }
}
function nonEmptyPlacement(value, label) {
    const trimmed = value.trim();
    if (!trimmed)
        throw new Error(`${label} is required.`);
    return trimmed;
}
function placementActionName(capability) {
    return capability.startsWith('spawn:') ? 'spawn' : capability;
}
function placementActionInput(input, placement) {
    const payload = { ...(input ?? {}) };
    payload.capability = placement.capability;
    payload.node = placement.node;
    payload.target_node = placement.node;
    if (placement.repo)
        payload.repo = placement.repo;
    if (placement.ttlMs > 0) {
        payload.ttl_override_ms = placement.ttlMs;
    }
    if (placement.capability.startsWith('spawn:')) {
        // The broker picks the harness from `cli`, but node eligibility was gated on
        // the `spawn:<cli>` capability. An explicit, mismatched `cli` would select a
        // harness the chosen node never advertised — reject it instead of silently
        // dispatching the wrong harness.
        const capabilityCli = placement.capability.slice('spawn:'.length);
        if (typeof payload.cli === 'string' && payload.cli !== capabilityCli) {
            throw new RelayPlacementError('capability_mismatch', `Placement rejected: input cli "${payload.cli}" does not match capability "${placement.capability}"`, { capability: placement.capability, node: placement.node, repo: placement.repo, attempts: 0 });
        }
        payload.cli = capabilityCli;
    }
    return payload;
}
function delay(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
}
/** Normalize a relaycast invoke ack (camelized) into the relay `RelayActionInvocationAck`. */
function normalizeActionInvocationAck(raw) {
    const record = asRecord(raw);
    return {
        invocationId: readStr(record, 'invocationId', 'invocation_id') ?? '',
        actionName: readStr(record, 'actionName', 'action_name') ?? '',
        ...(readStr(record, 'handlerAgentId', 'handler_agent_id')
            ? { handlerAgentId: readStr(record, 'handlerAgentId', 'handler_agent_id') }
            : {}),
        ...(readStr(record, 'handlerNodeId', 'handler_node_id')
            ? { handlerNodeId: readStr(record, 'handlerNodeId', 'handler_node_id') }
            : {}),
        ...(readStr(record, 'dispatchedNodeId', 'dispatched_node_id')
            ? { dispatchedNodeId: readStr(record, 'dispatchedNodeId', 'dispatched_node_id') }
            : {}),
        ...(readRecord(record, 'input') ? { input: readRecord(record, 'input') } : {}),
        ...(readStr(record, 'status') ? { status: readStr(record, 'status') } : {}),
        ...(readStr(record, 'createdAt', 'created_at')
            ? { createdAt: readStr(record, 'createdAt', 'created_at') }
            : {}),
    };
}
/** Normalize a relaycast invocation record (camelized) into `RelayActionInvocation`. */
function normalizeActionInvocation(raw) {
    const record = asRecord(raw);
    return {
        invocationId: readStr(record, 'invocationId', 'invocation_id') ?? '',
        actionName: readStr(record, 'actionName', 'action_name') ?? '',
        callerId: (readStr(record, 'callerId', 'caller_id') ?? null),
        callerName: (readStr(record, 'callerName', 'caller_name') ?? null),
        input: readRecord(record, 'input') ?? {},
        output: readRecord(record, 'output') ?? null,
        status: readStr(record, 'status') ?? 'invoked',
        error: (readStr(record, 'error') ?? null),
        durationMs: typeof record.durationMs === 'number'
            ? record.durationMs
            : typeof record.duration_ms === 'number'
                ? record.duration_ms
                : null,
        ...(readStr(record, 'createdAt', 'created_at')
            ? { createdAt: readStr(record, 'createdAt', 'created_at') }
            : {}),
        completedAt: (readStr(record, 'completedAt', 'completed_at') ?? null),
    };
}
/** Normalize a relaycast inbound webhook (snake_case) into `RelayInboundWebhook`. */
function normalizeInboundWebhook(raw) {
    const record = asRecord(raw);
    return {
        webhookId: readStr(record, 'webhookId', 'webhook_id', 'id') ?? '',
        url: readStr(record, 'url') ?? '',
        token: readStr(record, 'token') ?? '',
        channel: readStr(record, 'channel') ?? '',
        ...(readStr(record, 'name') ? { name: readStr(record, 'name') } : {}),
        ...(readStr(record, 'createdAt', 'created_at')
            ? { createdAt: readStr(record, 'createdAt', 'created_at') }
            : {}),
    };
}
/** Normalize a relaycast event subscription into `RelayWebhookSubscription`. */
function normalizeWebhookSubscription(raw) {
    const record = asRecord(raw);
    const events = Array.isArray(record.events)
        ? record.events.filter((event) => typeof event === 'string')
        : undefined;
    return {
        id: readStr(record, 'id') ?? '',
        ...(readStr(record, 'url') ? { url: readStr(record, 'url') } : {}),
        ...(events ? { events } : {}),
        ...(readStr(record, 'createdAt', 'created_at')
            ? { createdAt: readStr(record, 'createdAt', 'created_at') }
            : {}),
    };
}
function definedOptions(options) {
    return Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined));
}
function toMessageListOptions(options) {
    if (!options)
        return undefined;
    return definedOptions({
        limit: options.limit,
        before: options.before,
        after: options.after,
    });
}
function serializeAttachmentInputs(input) {
    if (!input)
        return undefined;
    return input.map((attachment) => typeof attachment === 'string' ? attachment : JSON.stringify(attachment));
}
function createRelaycastClient(options) {
    if (options.relaycast)
        return options.relaycast;
    const workspaceKey = options.workspaceKey ?? options.apiKey;
    if (!workspaceKey) {
        throw new Error('RelaycastMessagingClient requires workspaceKey when relaycast is not provided.');
    }
    return new RelayCast(definedOptions({
        apiKey: workspaceKey,
        baseUrl: options.baseUrl,
        retryPolicy: options.retryPolicy,
        ...relaycastTelemetryOptions({
            originActor: options.originActor,
            agentRelayDistinctId: options.agentRelayDistinctId,
        }),
    }));
}
export class RelaycastMessagingClient {
    capabilities;
    relaycast;
    agentClient;
    selfNodeName;
    placementTtlMs;
    maxQueuedPlacements;
    placementLog;
    queuedPlacements = 0;
    eventHandlers = new Map();
    eventUnsubscribe;
    constructor(options) {
        this.relaycast = createRelaycastClient(options);
        this.agentClient =
            options.agentClient ??
                (options.agentToken ? this.relaycast.as?.(options.agentToken, options.agentClientOptions) : undefined);
        this.selfNodeName = options.selfNodeName;
        this.placementTtlMs = options.placementTtlMs ?? 60 * 60 * 1000;
        this.maxQueuedPlacements = options.maxQueuedPlacements ?? 100;
        this.placementLog = options.placementLog;
        // Durable delivery state is agent-scoped: it requires an agent client that
        // exposes the relaycast delivery ledger (deliveries list + transitions).
        const durable = this.deliverySurface() !== undefined;
        this.capabilities = {
            serverDeliveryState: durable,
            durableDelivery: durable,
            durableAck: durable,
            durableFail: durable,
            durableDefer: durable,
        };
    }
    agents = {
        list: async (options) => {
            const agents = await this.relaycast.agents.list(definedOptions({ status: options?.status }));
            return agents.map(normalizeAgent);
        },
        get: async (name) => normalizeAgent(await this.relaycast.agents.get(name)),
        register: async (input) => normalizeAgentRegistration(await this.relaycast.agents.register(input)),
        registerOrRotate: async (input) => normalizeAgentRegistration(this.relaycast.agents.registerOrRotate
            ? await this.relaycast.agents.registerOrRotate(input)
            : await this.relaycast.agents.register(input)),
        me: async () => normalizeAgent(await this.requireAgentClient('agents.me').me()),
        update: async (name, input) => normalizeAgent(await this.relaycast.agents.update(name, input)),
        delete: async (name) => {
            await this.relaycast.agents.delete(name);
        },
        presence: async () => {
            const presence = await this.relaycast.agents.presence();
            return presence.map(normalizeAgentPresence);
        },
    };
    channels = {
        list: async (options) => {
            const channels = await this.relaycast.channels.list(definedOptions({ includeArchived: options?.includeArchived }));
            return channels.map(normalizeChannel);
        },
        get: async (name) => normalizeChannel(await this.relaycast.channels.get(normalizeChannelName(name))),
        create: async (input) => normalizeChannel(await this.requireAgentClient('channels.create').channels.create(input)),
        update: async (name, input) => normalizeChannel(await this.requireAgentClient('channels.update').channels.update(normalizeChannelName(name), input)),
        archive: async (name) => {
            await this.requireAgentClient('channels.archive').channels.archive(normalizeChannelName(name));
        },
        join: async (name) => {
            await this.requireAgentClient('channels.join').channels.join(normalizeChannelName(name));
        },
        leave: async (name) => {
            await this.requireAgentClient('channels.leave').channels.leave(normalizeChannelName(name));
        },
        invite: async (channel, agent) => {
            await this.requireAgentClient('channels.invite').channels.invite(normalizeChannelName(channel), agent);
        },
        members: async (name) => {
            const members = await this.requireAgentClient('channels.members').channels.members(normalizeChannelName(name));
            return members.map(normalizeChannelMember);
        },
        mute: async (name) => {
            await this.requireAgentClient('channels.mute').channels.mute(normalizeChannelName(name));
        },
        unmute: async (name) => {
            await this.requireAgentClient('channels.unmute').channels.unmute(normalizeChannelName(name));
        },
    };
    messages = {
        send: async (input) => {
            const message = await this.requireAgentClient('messages.send').send(input.channel, input.text, definedOptions({
                attachments: serializeAttachmentInputs(input.attachments),
                blocks: input.blocks,
                mode: input.mode,
                idempotencyKey: input.idempotencyKey,
            }));
            return normalizeMessage(message, { kind: 'channel', channelName: normalizeChannelName(input.channel) });
        },
        list: async (channel, options) => {
            const channelName = normalizeChannelName(channel);
            const messages = await this.relaycast.messages.list(channelName, toMessageListOptions(options));
            return messages.map((message) => normalizeMessage(message, { kind: 'channel', channelName }));
        },
        get: async (id) => normalizeMessage(await this.relaycast.messages.get(id)),
        reply: async (input) => {
            const message = await this.requireAgentClient('messages.reply').reply(input.messageId, input.text, definedOptions({ blocks: input.blocks, idempotencyKey: input.idempotencyKey }));
            return normalizeMessage(message, {
                kind: 'thread_reply',
                parentId: input.messageId,
                threadId: input.messageId,
            });
        },
        direct: async (input) => {
            const response = await this.requireAgentClient('messages.direct').dm(input.to, input.text, definedOptions({
                attachments: serializeAttachmentInputs(input.attachments),
                mode: input.mode,
                idempotencyKey: input.idempotencyKey,
            }));
            return this.normalizeDirectResponse(response, 'dm');
        },
        groupDirect: async (input) => {
            const agent = this.requireAgentClient('messages.groupDirect');
            const conversationId = input.conversationId ??
                normalizeGroupDirectConversation(await agent.dms.createGroup({ participants: input.participants ?? [], ...(input.name ? { name: input.name } : {}) }, definedOptions({ idempotencyKey: input.idempotencyKey }))).id;
            if (!conversationId) {
                throw new Error('messages.groupDirect requires conversationId or participants that create a conversation.');
            }
            const response = await agent.dms.sendMessage(conversationId, input.text, definedOptions({
                attachments: serializeAttachmentInputs(input.attachments),
                mode: input.mode,
                idempotencyKey: input.idempotencyKey,
            }));
            return this.normalizeDirectResponse(response, 'group_dm', conversationId);
        },
        createGroupDirect: async (input) => normalizeGroupDirectConversation(await this.requireAgentClient('messages.createGroupDirect').dms.createGroup({ participants: input.participants, ...(input.name ? { name: input.name } : {}) }, definedOptions({ idempotencyKey: input.idempotencyKey }))),
        listDirect: async (input) => {
            const options = toMessageListOptions(input);
            const list = this.agentClient
                ? await this.agentClient.dms.messages(input.conversationId, options)
                : await this.requireWorkspaceDmMessages()(input.conversationId, options);
            return list.map((message) => normalizeMessage(message, { kind: 'dm', conversationId: input.conversationId }));
        },
        markRead: async (messageId) => normalizeReadReceipt(await this.requireAgentClient('messages.markRead').markRead(messageId)),
        readers: async (messageId) => {
            const readers = await this.requireAgentClient('messages.readers').readers(messageId);
            return readers.map((reader) => normalizeReadReceipt({ ...(reader ?? {}), messageId }));
        },
        readStatus: async (channel) => {
            const statuses = await this.requireAgentClient('messages.readStatus').readStatus(normalizeChannelName(channel));
            return statuses.map(normalizeChannelReadStatus);
        },
        reactions: async (messageId) => {
            const reactions = this.agentClient
                ? await this.agentClient.reactions(messageId)
                : await this.relaycast.messages.reactions(messageId);
            return reactions.map(normalizeReaction);
        },
        react: async (messageId, emoji) => normalizeReaction(await this.requireAgentClient('messages.react').react(messageId, emoji)),
        unreact: async (messageId, emoji) => {
            await this.requireAgentClient('messages.unreact').unreact(messageId, emoji);
        },
        search: async (query, options) => {
            const results = await this.requireAgentClient('messages.search').search(query, definedOptions({
                channel: options?.channel ? normalizeChannelName(options.channel) : undefined,
                from: options?.from,
                limit: options?.limit,
                before: options?.before,
                after: options?.after,
            }));
            return results.map(normalizeSearchResult);
        },
    };
    threads = {
        get: async (messageId, options) => normalizeThread(await this.relaycast.messages.thread(messageId, toMessageListOptions(options))),
        reply: async (input) => this.messages.reply(input),
    };
    inbox = {
        get: async (options) => normalizeInbox(await this.requireAgentClient('inbox.get').inbox(definedOptions({ limit: options?.limit }))),
        /**
         * List durable deliveries queued for the authenticated agent. The
         * relaycast ledger replays non-terminal items (accepted + deferred) in
         * FIFO order with the message payload embedded. The underlying API has no
         * cursor, so `nextCursor` is never set and `before`/`after` are ignored.
         */
        list: async (input) => {
            const surface = this.deliverySurface();
            if (!surface)
                return { items: [] };
            const deliveries = await surface.deliveries(definedOptions({ limit: input?.limit }));
            return {
                items: deliveries.map((delivery) => normalizeInboxItem(delivery, definedOptions({ recipientName: input?.agentName }))),
            };
        },
        /**
         * Stream durable deliveries: seed from the non-terminal queue, then push
         * items announced by `delivery.accepted` WebSocket events, deduplicated by
         * delivery id. Falls back to an empty stream without an agent client.
         */
        subscribe: (input) => {
            const surface = this.deliverySurface();
            if (!surface)
                return this.emptyInboxSubscription();
            return this.createInboxSubscription(surface, input);
        },
        ack: async (input) => {
            const surface = this.deliverySurface();
            if (!surface)
                return this.unsupportedInboxDelivery('ack', input.inboxItemId);
            return normalizeDeliveryTransition('ack', await surface.ackDelivery(input.inboxItemId));
        },
        fail: async (input) => {
            const surface = this.deliverySurface();
            if (!surface)
                return this.unsupportedInboxDelivery('fail', input.inboxItemId, input.error);
            return normalizeDeliveryTransition('fail', await surface.failDelivery(input.inboxItemId, definedOptions({ error: input.error, retryable: input.retry })));
        },
        defer: async (input) => {
            const surface = this.deliverySurface();
            if (!surface) {
                return this.unsupportedInboxDelivery('defer', input.inboxItemId, input.reason, input.availableAt);
            }
            return normalizeDeliveryTransition('defer', await surface.deferDelivery(input.inboxItemId, {
                availableAt: input.availableAt,
                ...(input.reason === undefined ? {} : { reason: input.reason }),
            }));
        },
        markRead: async (input) => this.unsupportedInboxDelivery('ack', input.inboxItemId, 'The Relaycast delivery ledger has no read state; use inbox.ack to mark a delivery handled.'),
    };
    events = {
        connect: () => {
            if (this.eventUnsubscribe)
                return;
            const forward = (event) => this.emitEvent(normalizeMessagingEvent(event));
            // Agent-scoped clients stream through their own connection; a workspace-key
            // client streams all workspace-visible events through the workspace stream.
            if (this.agentClient) {
                this.agentClient.connect();
                this.eventUnsubscribe = this.agentClient.on.any(forward);
                return;
            }
            if (typeof this.relaycast.connect === 'function' && this.relaycast.on) {
                this.relaycast.connect();
                this.eventUnsubscribe = this.relaycast.on.any(forward);
                return;
            }
            // No agent client and no workspace stream available: preserve the
            // explicit "needs an agent token" error.
            this.requireAgentClient('events.connect');
        },
        disconnect: async () => {
            this.eventUnsubscribe?.();
            this.eventUnsubscribe = undefined;
            if (this.agentClient) {
                await this.agentClient.disconnect();
            }
            else if (typeof this.relaycast.disconnect === 'function') {
                this.relaycast.disconnect();
            }
        },
        subscribe: (channels) => {
            this.requireAgentClient('events.subscribe').subscribe(channels.map(normalizeChannelName));
        },
        unsubscribe: (channels) => {
            this.requireAgentClient('events.unsubscribe').unsubscribe(channels.map(normalizeChannelName));
        },
        on: (event, handler) => this.addEventListener(event, handler),
    };
    /**
     * Durable delivery transitions keyed by the relaycast delivery id (the
     * `InboxItem.id` returned by `inbox.list`/`inbox.subscribe`). Transitions
     * are idempotent on the server.
     */
    deliveries = {
        ack: async (deliveryId) => {
            const surface = this.deliverySurface();
            if (!surface)
                return this.unsupportedInboxDelivery('ack', deliveryId);
            return normalizeDeliveryTransition('ack', await surface.ackDelivery(deliveryId));
        },
        fail: async (deliveryId, reason) => {
            const surface = this.deliverySurface();
            if (!surface)
                return this.unsupportedInboxDelivery('fail', deliveryId, reason);
            return normalizeDeliveryTransition('fail', await surface.failDelivery(deliveryId, definedOptions({ error: reason })));
        },
        defer: async (deliveryId, deferUntil) => {
            const surface = this.deliverySurface();
            if (!surface)
                return this.unsupportedInboxDelivery('defer', deliveryId, undefined, deferUntil);
            return normalizeDeliveryTransition('defer', await surface.deferDelivery(deliveryId, {
                // The relaycast defer transition requires an explicit availability
                // time; default to a short retry window when none is given.
                availableAt: deferUntil ?? new Date(Date.now() + 30_000).toISOString(),
            }));
        },
    };
    integrations = {
        webhooks: {
            create: async (input) => (await this.requireWebhooks().create(input)),
            list: async () => (await this.requireWebhooks().list()),
            delete: async (id) => {
                await this.requireWebhooks().delete(id);
            },
            trigger: async (id, payload) => this.requireWebhooks().trigger(id, payload ?? {}),
        },
        subscriptions: {
            create: async (input) => (await this.requireSubscriptions().create(input)),
            list: async () => (await this.requireSubscriptions().list()),
            get: async (id) => (await this.requireSubscriptions().get(id)),
            delete: async (id) => {
                await this.requireSubscriptions().delete(id);
            },
        },
    };
    webhooks = {
        createInbound: async (input) => normalizeInboundWebhook(await this.requireWebhooks().createInbound({
            channel: normalizeChannelName(input.channel),
            ...(input.name === undefined ? {} : { name: input.name }),
        })),
        subscribe: async (input) => normalizeWebhookSubscription(await this.requireSubscriptions().create(definedOptions({
            url: input.url,
            events: input.events,
            secret: input.secret,
            headers: input.headers,
        }))),
        list: async () => (await this.requireWebhooks().list()).map(normalizeInboundWebhook),
        delete: async (webhookId) => {
            await this.requireWebhooks().delete(webhookId);
        },
        subscriptions: async () => (await this.requireSubscriptions().list()).map(normalizeWebhookSubscription),
        unsubscribe: async (id) => {
            await this.requireSubscriptions().delete(id);
        },
    };
    commands = {
        register: async (input) => toRelayCapability(await this.requireActions().register(toRegisterActionRequest(input))),
        list: async () => (await this.requireActions().list()).map(toRelayCapability),
        delete: async (command) => {
            await this.requireActions().delete(command);
        },
        available: () => Boolean(this.relaycast.actions),
        agentScoped: () => Boolean(this.agentClient?.actions),
        invoke: async (name, input) => normalizeActionInvocationAck(await this.requireAgentActions('commands.invoke').invoke(name, input)),
        getInvocation: async (name, invocationId) => normalizeActionInvocation(await this.requireAgentActions('commands.getInvocation').getInvocation(name, invocationId)),
        completeInvocation: async (name, invocationId, data) => normalizeActionInvocation(await this.requireAgentActions('commands.completeInvocation').completeInvocation(name, invocationId, toCompleteInvocationRequest(data))),
    };
    nodes = {
        list: async (options) => (await this.requireNodes().list(options)).map(toRelayNode),
        get: async (name) => {
            const nodes = this.requireNodes();
            if (nodes.get) {
                const raw = await nodes.get(name);
                return raw ? toRelayNode(raw) : null;
            }
            const [node] = (await nodes.list({ name })).map(toRelayNode);
            return node ?? null;
        },
    };
    placement = {
        spawn: async (input) => {
            const capability = nonEmptyPlacement(input.capability, 'placement capability');
            const repo = input.repo?.trim() || undefined;
            const targetNode = this.resolvePlacementNode(input.node, input.selfNodeName);
            const ttlMs = Math.max(0, input.ttlMs ?? input.ttlOverrideMs ?? this.placementTtlMs);
            const pollIntervalMs = Math.max(25, input.pollIntervalMs ?? 1_000);
            const startedAt = Date.now();
            let queued = false;
            let attempts = 0;
            try {
                while (true) {
                    attempts += 1;
                    const decision = await this.selectPlacementNode({ capability, repo, targetNode });
                    if (decision.node) {
                        const actionName = input.actionName ?? placementActionName(capability);
                        const actionInput = placementActionInput(input.input, {
                            capability,
                            node: decision.node.name,
                            repo,
                            ttlMs,
                        });
                        const ack = await this.commands.invoke(actionName, actionInput);
                        return {
                            ...ack,
                            node: decision.node,
                            placement: {
                                capability,
                                node: decision.node.name,
                                ...(repo ? { repo } : {}),
                                attempts,
                                queued,
                            },
                        };
                    }
                    if (decision.hardFail) {
                        this.logPlacement(input, decision.message);
                        throw new RelayPlacementError(decision.reason, decision.message, {
                            capability,
                            node: targetNode,
                            repo,
                            attempts,
                        });
                    }
                    if (input.failFast || Date.now() - startedAt >= ttlMs) {
                        // A repo that no live, capable node maps will never drain by waiting,
                        // so report it as `unmapped_repo` rather than a generic TTL expiry.
                        const code = decision.reconcileReason === 'unmapped_repo' ? 'unmapped_repo' : 'placement_ttl_expired';
                        const message = code === 'unmapped_repo'
                            ? `${decision.message}; no node maps the requested repo`
                            : `${decision.message}; placement TTL expired`;
                        await this.reconcilePlacement(input, {
                            action: 'failed',
                            reason: decision.reconcileReason,
                            capability,
                            ...(targetNode ? { node: targetNode } : {}),
                            ...(repo ? { repo } : {}),
                            attempts,
                            message,
                        });
                        throw new RelayPlacementError(code, message, {
                            capability,
                            node: targetNode,
                            repo,
                            attempts,
                        });
                    }
                    if (!queued) {
                        if (this.queuedPlacements >= this.maxQueuedPlacements) {
                            const message = `${decision.message}; placement queue full`;
                            await this.reconcilePlacement(input, {
                                action: 'failed',
                                reason: decision.reconcileReason,
                                capability,
                                ...(targetNode ? { node: targetNode } : {}),
                                ...(repo ? { repo } : {}),
                                attempts,
                                message,
                            });
                            throw new RelayPlacementError('placement_queue_full', message, {
                                capability,
                                node: targetNode,
                                repo,
                                attempts,
                            });
                        }
                        this.queuedPlacements += 1;
                        queued = true;
                        await this.reconcilePlacement(input, {
                            action: 'queued',
                            reason: decision.reconcileReason,
                            capability,
                            ...(targetNode ? { node: targetNode } : {}),
                            ...(repo ? { repo } : {}),
                            attempts,
                            message: decision.message,
                        });
                    }
                    // Floor the queued delay at a small minimum so a near-zero remaining
                    // TTL cannot busy-spin the poll loop before the next expiry check.
                    await delay(Math.max(5, Math.min(pollIntervalMs, ttlMs - (Date.now() - startedAt))));
                }
            }
            finally {
                if (queued)
                    this.queuedPlacements = Math.max(0, this.queuedPlacements - 1);
            }
        },
    };
    triggers = {
        list: async () => (await this.requireTriggers().list()).map(toRelayTrigger),
        create: async (input) => toRelayTrigger(await this.requireTriggers().create(toTriggerRequest(input))),
        update: async (id, input) => toRelayTrigger(await this.requireTriggers().update(id, toTriggerRequest(input))),
        delete: async (id) => {
            await this.requireTriggers().delete(id);
        },
    };
    workspace = {
        info: async () => {
            if (!this.relaycast.workspace) {
                throw new Error('RelaycastMessagingClient.workspace.info requires the relaycast workspace API.');
            }
            return (await this.relaycast.workspace.info());
        },
    };
    resolvePlacementNode(node, selfNodeName) {
        if (!node)
            return undefined;
        if (node !== 'self')
            return nonEmptyPlacement(node, 'placement node');
        const resolved = selfNodeName ?? this.selfNodeName;
        if (!resolved) {
            throw new Error('placement node "self" requires selfNodeName on the request or client.');
        }
        return nonEmptyPlacement(resolved, 'placement self node');
    }
    async selectPlacementNode(input) {
        if (input.targetNode) {
            const node = await this.nodes.get(input.targetNode);
            if (!node) {
                return {
                    message: `Placement queued: target node "${input.targetNode}" is not registered`,
                    reconcileReason: 'target_offline',
                };
            }
            if (!this.nodeHasCapability(node, input.capability)) {
                return {
                    message: `Placement rejected: node "${node.name}" does not advertise capability "${input.capability}"`,
                    hardFail: true,
                    reason: 'capability_mismatch',
                    reconcileReason: 'no_eligible_node',
                };
            }
            if (!node.live) {
                return {
                    message: `Placement queued: target node "${node.name}" is offline`,
                    reconcileReason: 'target_offline',
                };
            }
            if (!this.nodeMapsRepo(node, input.repo)) {
                return {
                    message: `Placement queued: node "${node.name}" does not map repo "${input.repo}"`,
                    reconcileReason: 'unmapped_repo',
                };
            }
            return { node };
        }
        const nodes = await this.nodes.list({ capability: input.capability });
        const capable = nodes.filter((node) => this.nodeHasCapability(node, input.capability));
        const live = capable.filter((node) => node.live);
        const eligible = live.filter((node) => this.nodeMapsRepo(node, input.repo));
        if (eligible[0])
            return { node: eligible[0] };
        if (input.repo && live.length > 0) {
            return {
                message: `Placement queued: no live node advertising "${input.capability}" maps repo "${input.repo}"`,
                reconcileReason: 'unmapped_repo',
            };
        }
        return {
            message: `Placement queued: no live node advertises capability "${input.capability}"`,
            reconcileReason: 'no_eligible_node',
        };
    }
    nodeHasCapability(node, capability) {
        return node.capabilities.some((item) => item.name === capability);
    }
    nodeMapsRepo(node, repo) {
        if (!repo)
            return true;
        return Boolean(node.repoKeys?.includes(repo));
    }
    async reconcilePlacement(input, event) {
        this.logPlacement(input, event.message);
        // A throwing/rejecting reconcile hook (e.g. a Slack/log sink outage) must not
        // break an otherwise valid placement — isolate it and log the failure.
        try {
            await input.onReconcile?.(event);
        }
        catch (error) {
            this.placementLog?.(`[agent-relay] placement reconcile hook threw: ${error instanceof Error ? error.message : String(error)}`);
        }
    }
    logPlacement(input, message) {
        const line = `[agent-relay] ${message}`;
        // Observability log sinks are caller-provided; never let them break placement.
        try {
            input.log?.(line);
        }
        catch (error) {
            this.placementLog?.(`[agent-relay] placement log hook threw: ${error instanceof Error ? error.message : String(error)}`);
        }
        if (input.log !== this.placementLog) {
            try {
                this.placementLog?.(line);
            }
            catch {
                // Intentionally swallow the client log-sink failure; nothing else to report to.
            }
        }
    }
    requireWebhooks() {
        if (!this.relaycast.webhooks) {
            throw new Error('RelaycastMessagingClient.integrations.webhooks requires the relaycast webhooks API.');
        }
        return this.relaycast.webhooks;
    }
    requireSubscriptions() {
        if (!this.relaycast.subscriptions) {
            throw new Error('RelaycastMessagingClient.integrations.subscriptions requires the relaycast subscriptions API.');
        }
        return this.relaycast.subscriptions;
    }
    requireActions() {
        if (!this.relaycast.actions) {
            throw new Error('RelaycastMessagingClient.commands requires the relaycast actions API.');
        }
        return this.relaycast.actions;
    }
    requireNodes() {
        if (!this.relaycast.nodes) {
            throw new Error('RelaycastMessagingClient.nodes requires the relaycast nodes API.');
        }
        return this.relaycast.nodes;
    }
    requireTriggers() {
        if (!this.relaycast.triggers) {
            throw new Error('RelaycastMessagingClient.triggers requires the relaycast triggers API.');
        }
        return this.relaycast.triggers;
    }
    requireAgentActions(operation) {
        const actions = this.agentClient?.actions;
        if (!actions) {
            throw new Error(`RelaycastMessagingClient.${operation} requires an agent-scoped client with the actions API.`);
        }
        return actions;
    }
    requireAgentClient(operation) {
        if (!this.agentClient) {
            throw new Error(`RelaycastMessagingClient.${operation} requires agentToken or agentClient.`);
        }
        return this.agentClient;
    }
    requireWorkspaceDmMessages() {
        if (!this.relaycast.dmMessages) {
            throw new Error('RelaycastMessagingClient.messages.listDirect requires agentClient or relaycast.dmMessages.');
        }
        return this.relaycast.dmMessages;
    }
    /**
     * The durable delivery API of the agent client, when present. Requires an
     * agent-scoped client built from `@relaycast/sdk` 2.5+ (or a compatible
     * injected `agentClient`).
     */
    deliverySurface() {
        const agent = this.agentClient;
        if (!agent ||
            typeof agent.deliveries !== 'function' ||
            typeof agent.ackDelivery !== 'function' ||
            typeof agent.failDelivery !== 'function' ||
            typeof agent.deferDelivery !== 'function') {
            return undefined;
        }
        return agent;
    }
    async *createInboxSubscription(agent, input) {
        const signal = input?.signal;
        if (signal?.aborted)
            return;
        const recipient = definedOptions({ recipientName: input?.agentName });
        const seen = new Set();
        const queue = [];
        let stopped = false;
        let notify;
        const wake = () => {
            const resolve = notify;
            notify = undefined;
            resolve?.();
        };
        const push = (item) => {
            if (!item.id || seen.has(item.id))
                return;
            seen.add(item.id);
            queue.push(item);
            wake();
        };
        const stop = () => {
            stopped = true;
            wake();
        };
        // Register the event listener before seeding so accepted deliveries that
        // land mid-seed are not missed; `seen` deduplicates the overlap.
        const inFlight = new Set();
        agent.connect();
        const unsubscribe = agent.on.any((event) => {
            const record = asRecord(event);
            if (record.type !== 'delivery.accepted')
                return;
            const deliveryId = readStr(record, 'deliveryId', 'delivery_id');
            if (!deliveryId || seen.has(deliveryId) || inFlight.has(deliveryId))
                return;
            inFlight.add(deliveryId);
            // The accepted event carries ids only; re-list the non-terminal queue
            // to pick up the delivery row with its embedded message payload.
            void agent
                .deliveries()
                .then((deliveries) => {
                const match = deliveries.find((raw) => readStr(asRecord(raw), 'id') === deliveryId);
                if (match)
                    push(normalizeInboxItem(match, recipient));
            })
                .catch(() => {
                // The delivery already transitioned or the list failed transiently;
                // it will be replayed by the next non-terminal listing.
            })
                .finally(() => {
                inFlight.delete(deliveryId);
            });
        });
        signal?.addEventListener('abort', stop, { once: true });
        try {
            for (const raw of await agent.deliveries()) {
                push(normalizeInboxItem(raw, recipient));
            }
            while (!stopped) {
                const next = queue.shift();
                if (next) {
                    yield next;
                    continue;
                }
                await new Promise((resolve) => {
                    notify = resolve;
                });
            }
        }
        finally {
            stopped = true;
            unsubscribe();
            signal?.removeEventListener('abort', stop);
        }
    }
    async *emptyInboxSubscription() {
        return;
    }
    unsupportedInboxDelivery(action, messageId, reason, deferUntil) {
        return {
            supported: false,
            action,
            messageId,
            ...(reason
                ? { reason }
                : {
                    reason: 'Durable delivery transitions require an agent-scoped client with the Relaycast delivery API.',
                }),
            ...(deferUntil ? { deferUntil } : {}),
        };
    }
    normalizeDirectResponse(input, kind, conversationId) {
        const record = input !== null && typeof input === 'object' && !Array.isArray(input)
            ? input
            : {};
        const resolvedConversationId = conversationId ??
            (typeof record.conversationId === 'string'
                ? record.conversationId
                : typeof record.conversation_id === 'string'
                    ? record.conversation_id
                    : undefined);
        const createdAt = typeof record.createdAt === 'string'
            ? record.createdAt
            : typeof record.created_at === 'string'
                ? record.created_at
                : undefined;
        return normalizeMessage(record.message, {
            kind,
            conversationId: resolvedConversationId,
            createdAt,
        });
    }
    addEventListener(event, handler) {
        let handlers = this.eventHandlers.get(event);
        if (!handlers) {
            handlers = new Set();
            this.eventHandlers.set(event, handlers);
        }
        const typedHandler = handler;
        handlers.add(typedHandler);
        return () => {
            const current = this.eventHandlers.get(event);
            if (current !== handlers)
                return;
            current.delete(typedHandler);
            if (current.size === 0) {
                this.eventHandlers.delete(event);
            }
        };
    }
    emitEvent(event) {
        const handlers = [
            ...(this.eventHandlers.get(event.type) ?? []),
            ...(this.eventHandlers.get('any') ?? []),
        ];
        for (const handler of handlers) {
            try {
                const result = handler(event);
                if (result && typeof result.catch === 'function') {
                    void result.catch((error) => {
                        console.error(`[agent-relay] messaging listener for "${event.type}" threw:`, error);
                    });
                }
            }
            catch (error) {
                console.error(`[agent-relay] messaging listener for "${event.type}" threw:`, error);
            }
        }
    }
}
//# sourceMappingURL=relaycast.js.map