export type InputDelivery = "steer" | "queue"; export type ResponseInput = { id: string; text: string; delivery: InputDelivery; status: "pending" | "delivered" }; type InboxRuntime = { enqueueUserInput: (sessionId: string, input: ResponseInput) => Promise; listPendingUserInputs: (sessionId: string) => Promise; }; const unavailable = () => Object.assign(new Error("The response is closing; send after it finishes"), { status: 409 }); /** Tracks native inbox receipts, never starts a second host generation loop. */ export class ResponseInputTracker { private readonly items = new Map(); private readonly operations = new Set>(); private accepting = true; private releaseReady!: () => void; private readonly ready = new Promise((resolve) => { this.releaseReady = resolve; }); constructor(private readonly runtime: InboxRuntime, private readonly sessionId: string, private readonly changed: (input: ResponseInput) => void) {} markReady() { this.releaseReady(); } close() { this.accepting = false; this.releaseReady(); } get hasAdditionalInput() { return this.items.size > 0; } async restore() { for (const input of await this.runtime.listPendingUserInputs(this.sessionId)) this.items.set(input.id, input); } async submit(input: ResponseInput): Promise { if (!this.accepting) throw unavailable(); const previous = this.items.get(input.id); if (previous) { if (previous.text !== input.text) throw Object.assign(new Error("Input ID already belongs to another message"), { status: 409 }); await Promise.all([...this.operations]); return { ...previous }; } if ([...this.items.values()].filter((item) => item.status === "pending").length >= 20) { throw Object.assign(new Error("At most 20 user messages may wait in this session"), { status: 429 }); } if (this.items.size >= 500) throw Object.assign(new Error("Start a new turn before adding more input"), { status: 429 }); this.items.set(input.id, input); const operation = (async () => { await this.ready; if (!this.accepting) throw unavailable(); await this.runtime.enqueueUserInput(this.sessionId, input); this.changed({ ...input }); return { ...input }; })(); this.operations.add(operation); try { return await operation; } catch (error) { if (input.status === "pending") this.items.delete(input.id); throw error; } finally { this.operations.delete(operation); } } observe(type: string, data: { inboxID?: string; delivery?: InputDelivery }) { const input = data.inboxID ? this.items.get(data.inboxID) : undefined; if (!input) return; if (type === "session.inbox.delivered") input.status = "delivered"; else if (type === "session.inbox.delivery.changed" && data.delivery) input.delivery = data.delivery; else return; this.changed({ ...input }); } async canClose(): Promise { await Promise.allSettled([...this.operations]); if ([...this.items.values()].some((input) => input.status === "pending")) return false; this.accepting = false; return true; } }