import type { ReleaseBlocker, StopAllBackgroundItem } from "@selesai/code"; import type { IntercomContext } from "./reply-tracker.ts"; export interface IntercomReleaseState { /** Inbound asks still awaiting OUR reply (`ReplyTracker.listPending()`). */ pendingAsks: readonly IntercomContext[]; /** The module-level blocking outbound ask (`replyWaiter`), if one is in flight. */ outboundAsk: { from: string; replyTo: string; startedAt?: number } | null; /** Outbound outbox requests from other extensions that have not settled. */ pendingOutboxRequests: number; } const clip = (value: string, max: number) => value.length > max ? value.slice(0, max) : value; /** Release blockers owned by the intercom extension. Pure: callers pass a snapshot of live state. */ export function intercomReleaseBlockers(state: IntercomReleaseState): ReleaseBlocker[] { const blockers: ReleaseBlocker[] = []; for (const ask of state.pendingAsks) { const sender = ask.from.name?.trim() || ask.from.id; blockers.push({ source: "intercom", kind: "inbound_ask", id: clip(ask.message.id, 256), detail: clip(`Ask from ${sender} is awaiting our reply`, 512), ...(Number.isFinite(ask.receivedAt) && ask.receivedAt >= 0 ? { since: Math.trunc(ask.receivedAt) } : {}), }); } if (state.outboundAsk) { const { from, replyTo, startedAt } = state.outboundAsk; blockers.push({ source: "intercom", kind: "outbound_ask", id: clip(replyTo, 256), detail: clip(`Blocking ask to ${from} is awaiting a reply`, 512), ...(startedAt !== undefined && Number.isFinite(startedAt) && startedAt >= 0 ? { since: Math.trunc(startedAt) } : {}), }); } if (state.pendingOutboxRequests > 0) { blockers.push({ source: "intercom", kind: "outbound_message", detail: `${state.pendingOutboxRequests} outbound message request(s) have not settled` }); } return blockers; } export interface IntercomStopState { /** Inbound asks still awaiting OUR reply (`ReplyTracker.listPending()`). */ pendingAsks: readonly IntercomContext[]; /** The module-level blocking outbound ask (`replyWaiter`), if one is in flight. */ outboundAsk: { from: string; replyTo: string; startedAt?: number } | null; /** * Cancels the outbound ask: tells the broker (`client.cancelAsk`, drops the ask edge and its pending-ask record) and rejects the * local waiter so the blocked tool call returns. Synchronous. Optionally returns the settled outcome of asking the recipient to * drop the message (`client.cancelMessage`); that part is best-effort because the recipient may already have acted on it. */ cancelOutboundAsk(ask: { from: string; replyTo: string }): void | PromiseLike; } /** * Kill-switch handler. Outbound asks have a cancel (broker `cancel_ask` / `cancel_message`, exposed by the client), so they are * cancelled. Inbound asks waiting on THIS session cannot be cancelled by the receiver (the protocol has no "decline"), so they are * left alone and reported as `ok:false` so the caller can show who is still waiting. Never throws. */ export function stopIntercom(state: IntercomStopState, request: { deadline: number }, now: () => number = Date.now): StopAllBackgroundItem[] | PromiseLike { const items: StopAllBackgroundItem[] = []; let pendingCancel: PromiseLike | undefined; const ask = state.outboundAsk; let outbound: StopAllBackgroundItem | undefined; if (ask) { const base = { source: "intercom", kind: "outbound_ask", id: clip(ask.replyTo, 256) }; try { const outcome = state.cancelOutboundAsk({ from: ask.from, replyTo: ask.replyTo }); outbound = { ...base, detail: clip(`Cancelled blocking ask to ${ask.from}`, 512), ok: true }; if (outcome) pendingCancel = outcome; } catch (error) { const text = error instanceof Error ? error.message : String(error); outbound = { ...base, detail: clip(`Failed to cancel blocking ask to ${ask.from}: ${text}`, 512), ok: false, error: clip(text, 256) }; } items.push(outbound); } for (const inbound of state.pendingAsks) { const sender = inbound.from.name?.trim() || inbound.from.id; items.push({ source: "intercom", kind: "inbound_ask", id: clip(inbound.message.id, 256), detail: clip(`cannot cancel inbound: ask from ${sender} is awaiting our reply and keeps waiting`, 512), ok: false, error: "cannot cancel inbound", }); } if (!pendingCancel || !outbound) return items; const finished = outbound; const budget = Math.max(0, request.deadline - now()); return new Promise((resolve) => { const timer = setTimeout(() => resolve(items), budget); pendingCancel!.then((note) => { clearTimeout(timer); if (note) finished.detail = clip(`${finished.detail}; ${note}`, 512); resolve(items); }, () => { clearTimeout(timer); resolve(items); }); }); }