import type { CoreEvent, SessionView } from '../core/types.js'; /** A cached open human gate for a run. */ export interface GateCacheEntry { /** The unit `ord` the run paused before. */ ord: number; /** The gate prompt (lives only on the transient event — core does not persist it). */ prompt: string; /** Lifecycle state. Only `open` in this foundation increment (see the class note). */ lifecycle: 'open'; /** When the daemon observed the gate. */ receivedAt: string; /** * Present ONLY when the gate prompt reads as a pure sandbox/tool refusal — the worker reported it * could not act, with no sign of productive work (issue #419). A warning for the operator, never a * gate decision; omitted entirely on a normal gate so the wire is byte-identical. */ refusal?: { matched: boolean; reason: string; }; /** * WHY the run paused (`awaitingHuman.gateKind`; the durable row's `gate_kind`) — * studio#814: without it a reloaded skin could only guess the gate from its unit, and read a * refused hand-over's escalation (cursor still on the deliver unit) as a fresh deliver gate. * ABSENT when the engine did not say (an older engine, a row written before the field). */ gateKind?: string; /** * The answer tokens the engine offered (`awaitingHuman.choices`; issue #969) — e.g. a * `team_dispute` gate's `['approve', 'request_changes', 'reject']`. Without them a client that * missed the live frame could draw only Approve/Reject. ABSENT when the engine offered none. */ choices?: string[]; /** The index into `choices` of the engine's recommended answer. ABSENT without `choices`. */ recommended?: number; } /** * Detect whether a gate prompt is a PURE sandbox/tool refusal — the worker reporting it could not * act (read-only sandbox, rejected writes, "could not modify/regenerate"), with NO sign it did real * work (issue #419). Returns `{ matched: true, reason }` only when a refusal signature fires AND no * work-done phrase vetoes it; `undefined` otherwise. This is a WARNING signal surfaced on the gate * wire so an operator does not approve a refusal as if it were work — it never gates a decision. * * The bias is deliberately toward NOT flagging: a genuine work transcript that merely mentions * "sandbox"/"blocked", or a mixed turn that refused one tool but did real work elsewhere, must not * be flagged. A missed refusal is a lesser harm than a false one. */ export declare function detectRefusal(text: string): { matched: boolean; reason: string; } | undefined; /** * The answer set an `awaitingHuman` frame offered, kept only when well-formed (issue #969): * `choices` must be a non-empty array of strings, and `recommended` an integer index inside it. * A malformed value is dropped rather than served — a skin would render it as buttons. Spread the * result, so a gate the engine offered nothing on stays byte-identical on the wire. */ export declare function offeredChoices(event: CoreEvent): { choices?: string[]; recommended?: number; }; /** * The open gate a run's event history records, or `undefined` — the last `awaitingHuman` not * followed by a resumed/terminal event. `events` is the run's log oldest-first. Pure: the same * `fold` the live stream uses, over a scratch map, so a replay cannot disagree with what the live * stream would have produced from the same events (FINDING-051). */ export declare function replayOpenGate(runId: string, events: CoreEvent[]): GateCacheEntry | undefined; /** * What one read of a run's open gate from the record yields: the gate; `null` (the record holds * no open gate); `'no-log'` (this build cannot read the log, so nothing can be said); * `'ambiguous'` (the engine's open gate rows disagree on the ord, so the record cannot say which * gate is open — crew#1083 round 5). */ export type GateRead = GateCacheEntry | null | 'no-log' | 'ambiguous'; /** * What `GateCache.resolve` yields: a read's result, or `'unstable'` — live gate events kept landing * while the record was read, so nothing could be verified (crew#1083 round 3). Retryable. */ export type GateResolution = GateRead | 'unstable'; /** Narrows a resolution to an open gate: `null`, `'no-log'`, `'ambiguous'` and `'unstable'` are not one. */ export declare function isOpenGate(resolution: GateResolution | undefined): resolution is GateCacheEntry; /** The one log call the cache makes (`resolve`): `console` by default, the daemon's logger in `server.ts`. */ export interface GateCacheLog { warn(message: string): void; } /** * Gate-prompt cache over a durable log (DES-STUDIO-001 §3.3). * * The prompt is keyed by runId — a paused run has exactly one open gate, before `unit_ix`. Entries * are pruned on any resumed/terminal event for the run, and re-checked against `sessionsDetail()` * on every list reconcile, so the cache can neither leak nor go stale. * * That self-healing claim was true and beside the point until FINDING-051: every path here was * prune-only, so a daemon restart dropped the map and **nothing could put an entry back**. A parked * run then kept holding the operator without being able to say what it was asking. The prompt was * never actually lost — core records `awaitingHuman` to the event log (core#139) — it was simply * never read. `resolve`, fed the route's read of the durable row and the log, is that read, which * demotes this map to what it was always meant to be: a latency optimisation, not the system of * record. * * NOTE: the full HITL lifecycle from §3.3 (advisory / hard-expiry timers, append-only message log, * auto-cancel-on-expiry, synthesized `gateLifecycle` frames) is a deliberate follow-on. This * increment caches only the prompt so a late-joining browser can render the gate; `lifecycle` is * always `open`. */ export declare class GateCache { private readonly log; private readonly entries; /** * Per-run count of the LIVE gate-opening and gate-closing events `ingest` has folded. `resolve` * compares it across an async read of the record: a read some event overlapped is run again, * never adopted. Never pruned — one number per run seen. */ private readonly revisions; constructor(log?: GateCacheLog); /** Fold one live CoreEvent into the cache. */ ingest(event: CoreEvent): void; private revision; /** * The run's open gate: the cached entry, else what `read` finds in the record — the durable row, * the log — adopted and returned once a read completes with no live gate event of the run's * having landed meanwhile. `read` must not consult this cache: it is run again when one does. * * Neither side of such an overlap is known to be the newer (crew#1083 review). The record may * have moved on since the read began — a live `awaitingHuman` for the next gate while an older * row was being read — or the engine's pump may have delivered an OLDER frame late: an * `awaitingHuman` for a gate the row already shows answered, or that gate's `resumed`. The * revision counts arrivals, not order, so the cache cannot pick a winner; the first cut let the * live side win and served a late ord-3 frame over a current ord-4 row, which let a decision * naming ord 3 pass the gate check and answer ord 4. So no winner is picked. The read is run * again, from scratch: the engine writes the row with the pause, so a re-read begun after the * overlapping frame arrived reflects whatever that frame announced, and the first read that * completes with nothing having landed is adopted. * * A clean read is the record's word in BOTH directions (crew#1083 round 3): a gate it finds * replaces whatever an overlapped `ingest` folded meanwhile, and "no open gate" / "cannot read" * EVICTS it — otherwise a late frame folded during attempt one outlived the clean attempt two * that found its gate already answered, and was served as a cache hit, unread, from then on. A * read that throws evicts the same way. After `RESOLVE_ATTEMPTS` overlapped reads nothing was * verified, so nothing is served: the entry is evicted and the result is `'unstable'`, never the * unverified entry — an ordinary ordered backlog (`awaitingHuman(2)`, `resumed(2)`, * `awaitingHuman(3)`, one landing per read) left the cache holding ord 3 while every read found * ord 4. The run is logged once. * * One read in flight per run (agy, round 4): while a read is running, an entry a live frame * folds is unverified, so a concurrent `resolve` awaits the read in flight instead of taking the * entry as a hit, and the entry is evicted before a re-read so a `get` meanwhile serves nothing * unverified either. */ resolve(runId: string, read: () => Promise): Promise; /** In-flight resolutions per run: `resolve` hands every concurrent caller the same one. */ private readonly inflight; private hydrate; /** * The cached open gate for a run, if any — and nothing while a read of it is in flight: an entry * a live frame folded meanwhile is unverified until that read settles, so a sync caller (the GET * route's hit path, a steering prompt) falls through to `resolve` and waits (agy, round 4). */ get(runId: string): GateCacheEntry | undefined; /** * Drop the run's cached gate: the engine's record shows another gate open, or none (crew#1083 * round 7). The next read hydrates from the record. */ forget(runId: string): void; /** * Seed the run's gate synchronously. Nothing can land between the caller's decision and this * set, so it needs no guard; a read that awaited anything goes through `resolve`. */ adopt(runId: string, entry: GateCacheEntry): void; /** Drop cache entries whose run is no longer `awaiting_human` (self-healing on reconcile). */ reconcile(views: SessionView[]): void; } //# sourceMappingURL=gate-cache.d.ts.map