/** * DES-ASK-TEAM-CHAT-001 §5.2 (ASK-C2) — the ask relay, crew's READ side of an ask path. * * After ASK-C1 an ask is a team run: the PA answers in an `understand` step `answer-N`, and the * engine publishes the run's unit frames (`unitDispatched`, `unitOutputDelta`, `unitOutputCaptured`, * `unitDone`, …) and team rows (`wicked.team.*`) — none of them a chat frame. This relay is the ONE * producer of chat frames for a path: it folds the answer unit's `unitOutputDelta` into * `chatDelta{chat, cliKey:}` (live typing only) and folds one `chatReply{chat, cliKey:, text, * ok, run_id, ord}` per answer unit whose text is the output the team row * `step.completed{step_id:"answer-N"}.output_ref` names — read through the engine's `work_output` at * the FOLD (`unitDone{ord}`), not at the row (the row is published by the worker before the actor * folds; the record exists once the fold ran — §5.2 "when the text is readable, verified"). * * The join is by ATTEMPT (codex on #810 r1, 2): `output_ref` is `unit:::`, and the * attempt the fold settles is the latest the engine dispatched for that ord (`unitDispatched`, * `unitOutputDelta`, `unitOutputCaptured` all carry it) — a failed attempt's row never decides a * successful retry. A fold whose row has not arrived (the bus poll is 2 s) waits for it; past * `rowGraceMs` the reply is read from the record anyway, with `ok` from the engine's own * `unitOutputCaptured.stepStatus` — the un-teamed run of §8 F7 publishes no team row at all, and its * answer still arrives (r1, 5). A RUN-terminal frame with no fold (`sessionFailed{ord}`, * `runCancelled`) ends the turn with `ok:false` (r1, 6); `stepFailed` / `unitDenied` are NOT * terminal — the engine fails over (a re-pick re-dispatches the ord, §8 F2) or opens an escalation * gate whose Approve re-dispatches it, and a failed run says so with `sessionFailed` (r2, 1). * * The PA: the team rows (`path.started`, `path.repicked`) are authoritative; the unit's assigned seat * is the fallback while none has arrived, and the unit record is re-read on every new attempt (a * re-pick re-dispatches the same ord on another seat — r1, 1). Control lines (`HELP:`, `PLAN+`, * `PLAN :`, `ADVICE :`, `STEP :`, `SCOPE`, `RISK`) are stripped from the displayed text * (§4.7); a delta stream that disagrees with the record is overwritten by it (the transcript's * `seat` record is written from the reply). * * Every frame goes through the server's event fold (`fold`), so the turn index stamps `turn_id`, the * transcript persists the reply, the decision recorder and the citation verifier see what they saw * for a pool chat, and /ws fans it out — unchanged consumers (§5.2, codex #4). Every await is * followed by a re-check that the run still belongs to the chat (End forgets it — r1, 4). Crew * publishes no bus row (T2 §7 assertion 4). The relay is in-memory: a daemon restart forgets which * run is which chat (the ask-path index is in-memory too), so a path that outlives the daemon is * read from the run view, not relayed. */ import type { AskPathIndex } from './ask-paths.js'; import type { ChatTurnIndex } from './chat-turns.js'; import type { BusEvent } from '../core/bus.js'; import type { CoreEvent } from '../core/types.js'; /** A unit as the relay needs it (the run view's `WorkUnit`, narrowed). */ export interface RelayUnit { id: string; ord: number; status: string; assigned_cli: string | null; /** The engine's persisted attempt counter for the unit (0-based), when the run view carries it: * the durable half of the attempt boundary (codex on #810 r4, 2). */ last_attempt?: number; } export interface AskRelayDeps { paths: AskPathIndex; turns: ChatTurnIndex; /** `Core::work_output(unit_id)` — the stored output of an approved unit, `null` when none. */ workOutput: (unitId: string) => Promise; /** The run's units (the run view) — resolved per (run, ord, attempt) and cached. */ units: (runId: string) => Promise; /** Where frames go: the server's event fold (stamp → transcript → recorder → /ws). Not a bus * publish — crew puts no row on the bus (tests/team-no-publish.test.ts). */ fold: (frame: CoreEvent) => void; /** The voice of a turn was learned (a random pick, a re-pick): the decision recorder's audience * follows the turn index's (r1, 7). */ recorderReconcile?: (chat: string, turnId: string, seats: readonly string[]) => void; /** How long a fold waits for its `step.completed` row before the reply is read from the record * anyway (the un-teamed run publishes none). Default 5 s. */ rowGraceMs?: number; log?: (msg: string) => void; } /** The displayed text: every control line removed, surrounding blank lines trimmed. */ export declare function stripControlLines(text: string): string; /** `unit:::` → `{ord, attempt}`, or `null` for any other shape. */ export declare function refOf(ref: string): { ord: number; attempt: number; } | null; /** `unit:::` → the ord, or `null` for any other shape. */ export declare function ordOfOutputRef(ref: string): number | null; /** The step id an answer unit's id carries (`:answer-N` → `answer-N`), or `null`. */ export declare function answerStepOf(runId: string, unitId: string): string | null; export declare class AskRelay { private readonly deps; /** run → ord → attempt → the `step.completed` row. */ private readonly rows; /** run → ord → the latest attempt the engine dispatched / streamed / captured. */ private readonly attempts; /** run → ord → the engine's own status of the latest captured attempt (`unitOutputCaptured`). */ private readonly captured; /** run → ord → the latest attempt's summed `cliUsage` (crew#824). */ private readonly usage; /** run → ord → the unit (id + seat), resolved lazily from the run view; re-read per attempt. */ private readonly units; /** `${run}:${ord}` → a fold waiting for its row (bounded by `rowGraceMs`). */ private readonly waiting; /** `${run}:${ord}` whose reply went out (or is going out) — exactly one per answer unit. */ private readonly replied; /** `${run}:${ord}` → the partial last line of the delta stream (a control line may split). */ private readonly tails; /** Runs a terminal frame has ended: no later fold, row, grace or read continuation may speak * for them (codex on #810 r10, 2). */ private readonly ended; /** `${run}:${ord}` whose reply was actually FOLDED (a reservation in `replied` may still be in * flight — a terminal must speak for it, r11, 1). */ private readonly spoken; private readonly rowGraceMs; constructor(deps: AskRelayDeps); private log; /** A `teamEvent` frame's bus row (the team /ws relay hands every row here). */ onTeamRow(row: BusEvent | undefined): void; /** A CoreEvent of the engine's stream — only the unit / terminal frames are read. */ onCoreEvent(event: CoreEvent): Promise; /** A fold with no row of its attempt yet: wait for the row (the bus poll is 2 s) — bounded by * `rowGraceMs`, after which the reply is read from the record anyway. */ private awaitRow; /** Forget a run (its chat closed). */ forget(runId: string): void; private nested; private nestedOne; private noteAttempt; /** The attempt the fold settles for (run, ord): the latest the engine's frames, the team rows or * the unit's persisted counter named — one monotonic boundary (r3, 1); attempts are 0-based * (r4, 1), so `undefined` = nothing has named one yet. */ private attemptOf; /** Whether a frame labelled `attempt` belongs to an attempt the boundary has moved past. */ private superseded; private rowFor; private stopWaiting; private learnPa; /** The PA: the unit's seat for the CURRENT attempt (the cache is dropped per attempt, so a read * unit is this attempt's — a re-pick shows here before its bus row does; r4, 3); the team rows * (`path.started` / `path.repicked`) when the unit names no seat yet. */ private paOf; /** `undefined`: a FRESH read failed — the durable record could not be certified (the caller * must not answer from the cache; r6, 1). `null`: no such unit. */ private unit; /** The complete lines of the stream so far, control lines removed; the partial tail is held. */ private deltaText; /** ONE reply per answer unit: the key is reserved before the first await (r1, 8). */ private reply; /** The seats a failure must be said for: the PA when the path knows it, else every seat the * chat's live turns still wait on (the reserved audience of a random pick), else `pa`. */ private voicesFor; /** The run view, retried a few times — a terminal fact must not be lost to one failed read. */ private unitsRetrying; /** A terminal frame with no fold: the answer unit(s) still pending end the turn with `ok:false`. * The run view is read with a bounded retry; if it still cannot be read, the turn is ended * without the unit's facts rather than left pending (r8, 2). */ private terminal; } //# sourceMappingURL=ask-relay.d.ts.map