/** * approval-broker-raise.ts, raising an ask, as a free function. * * ── Why this is not a method ─────────────────────────────────────────────── * * Two reasons, and the second is the load-bearing one. * * 1. approval-broker.ts sits within a few lines of the hand-authored source cap * and this is its longest single body, so it follows the split the session * broker already made (handleIntent → session-broker-intent.ts): a free * function over an explicit deps object, with a thin delegating method left * on the class. * * 2. Raising an ask now has TWO callers that want different things from it. The * in-process one (`requestApproval`) wants the decision and nothing else, * it is awaiting a human. The wire one (`approvals.raise`, routes/ * approvals-raise.ts) wants the RECORD, immediately, and must not hold an * HTTP request open across a person's attention span. Both are the same act, * so both go through this function; it returns the record AND the pending * decision and lets each caller keep the half it needs. * * ── The coalescing rule, restated because it is subtle ───────────────────── * * A second identical in-flight ask (same session, tool and args) does not * create a second record or a second prompt: it attaches to the first record's * pending promise, so one decision resolves both. The returned `approval` is * then the EXISTING record, which is exactly what a wire caller needs, because * the id it gets back is the id whose updates it will see on the stream. * `coalesced` says which happened, so a caller that raised an ask and got back * a record older than its call can tell that from a fresh one. */ import type { PermissionPromptDecision } from '../permissions/prompt.js'; import type { RequestSharedApprovalInput, SharedApprovalAuditRecord, SharedApprovalRecord } from './approval-broker.js'; /** A pending ask's resolvers and its expiry timer, as the broker holds them. */ export interface PendingApprovalEntry { readonly resolvers: ((decision: PermissionPromptDecision) => void)[]; readonly timer?: ReturnType | undefined; } /** Everything raising an ask needs from the broker that owns the state. */ export interface RaiseApprovalDeps { /** Load persisted state before touching it, the broker's own `start()`. */ start(): Promise; /** The live record map, keyed by approval id. */ readonly approvals: Map; /** The live pending-resolver map, keyed by approval id. */ readonly pendingResolvers: Map; /** Write the record map to the store. */ persist(): Promise; /** Fan the record out: SSE `approval-update`, surface message, listeners. */ publish(approval: SharedApprovalRecord): void; /** Expire an ask whose deadline passed. */ expire(approvalId: string, note: string): Promise; /** Audit-entry factory, the broker's own, so entries stay identical. */ buildAudit(action: SharedApprovalAuditRecord['action'], actor: string, actorSurface?: string, note?: string): SharedApprovalAuditRecord; /** The (session, tool, args) coalescing key, the broker's own. */ coalesceKey(sessionId: string | undefined, tool: string, args: Record): string; } /** A raised ask: the record it produced, and the decision still to come. */ export interface RaisedApproval { readonly approval: SharedApprovalRecord; readonly decision: Promise; /** True when this ask attached to an identical one already in flight. */ readonly coalesced: boolean; } /** * Raise an ask through the shared broker. * * Ordering is deliberate and unchanged from when this was a method: the record * is persisted BEFORE it is published and before any local prompt runs, and a * failed persist rolls the record and its resolver back out of both maps and * re-writes the corrected map to the file. An approval that a surface can see * and a restart cannot is the one failure this ordering exists to prevent. */ export declare function raiseSharedApproval(input: RequestSharedApprovalInput, deps: RaiseApprovalDeps): Promise; //# sourceMappingURL=approval-broker-raise.d.ts.map