import { type TerminalCause } from "@sema-agent/core"; import { brainStatusEventData } from "./project.js"; import { NotifiedKeys } from "../orchestration/workflow-completion-inbox.js"; import { type LedgerEventType, type ParkLedgerEventType } from "./ledger-events.js"; /** * LedgerSink — the SINGLE "stream event → durable runs-ledger" projection, shared by the background leg * (runs.ts `runInBackground`) and the sync `/v1/tasks/stream` detach lane (server.ts, `x-detach-on-disconnect`). * * [ref]①/[ref]③: the sync leg historically persisted only createRun+setTerminal — its events ledger stayed * EMPTY, so after a detach the shell could only poll the terminal row. Feeding the same sink on the detach * lane makes `GET /v1/runs/:id/events` replay the full stream in the EXACT bg-leg vocabulary (the ledger * consumer binds to these type/data shapes — a second copy of this switch would drift into a false ledger, * so never fork it). * * Scope: only the PURE transform lives here — text/reasoning coalescing, redaction, the per-run monotone * `seq`, and the type/data whitelist shapes. Everything leg-specific stays with the caller: rewind anchors, * fleet rows, metrics, model-usage deltas (E8), checkpoint parking and run-row settles. `done` and * `message_committed` are deliberately NO-OPS in {@link LedgerSink.onEvent} — the bg leg must interleave * model-usage writes between the text flush and the terminal event (it drives {@link LedgerSink.appendParked} * / {@link LedgerSink.appendDone} itself), and message_committed is a session-tree concern (replay walks the * persisted tree; it is never a ledger row). */ export interface LedgerSink { /** Seq-stamped raw append — for a leg's out-of-band rows (elicitation/question frames, model_usage deltas, * suggestions, `failed`) that must share this run's single seq counter. Redaction is the CALLER's duty on * this raw door (every existing site already redacts); the typed doors below redact internally. */ append(type: LedgerEventType, data: unknown): Promise; /** Flush the buffered turn text/reasoning. Thinking precedes content within a turn → a buffered reasoning * block flushes FIRST (redacted; core [R57]: CoT may carry reasoned-about secrets — TRACE_THINKING gates * capture). The `text` row is stamped with the turn's first text eventId (E18 per-message resume handle, * additive/tolerate-absent). Buffers clear only AFTER a successful write (a failed write drops nothing). */ flush(): Promise; /** The shared stream-switch projection (all non-terminal events). `done` / `message_committed` / unknown * future types are no-ops (whitelist discipline — never a raw spread into the ledger). */ onEvent(ev: { type: string; }): Promise; /** * [ref](core 5.34.0 的 `RunInternals.onStatusEvent` 第五席):**「只回结果」腿**的 `status` 行写口。 * * 与 {@link onEvent} 的 `case "status"` 共用**同一个**白名单 builder(`brainStatusEventData`),差别只有 * 一处:本门**同步入链** —— 在回调进入的那一刻就占住序号槽,而不是先 `await flush()` 再入链。 * * 🔴 **为什么必须同步入链**(codex 交叉复审 R2 [medium],红先复现后修):`onForwardEvent` 是同步入链的, * 而 core 的第五席与 forward sink 都是**同步回调**、可能同一拍相邻到达。走 `onEvent` 的话那一次 * `await flush()` 会让**后到**的 forward 帧抢到**更小**的序号 ⇒ durable 时间线倒挂(消费端读到「子代 * 进度」排在触发它的那次重连之前)。钉在 `test/issue190-status-wire-disclosure.test.ts` §4⑤b。 * * 🔴 **为什么可以不 flush**:本门只服务「只回结果」的两条腿(verify/cascade、resume-verify),它们的 * sink **从不收** `text_delta`/`reasoning_delta` —— `onForwardEvent` 有意不逐块持久化子代文本,而 * `onEvent` 在这些腿上根本没有调用点 ⇒ 没有可被倒挂的已缓冲文本。真有文本缓冲的腿(普通流形 / sync * detach / resume stream)走 `onEvent` 或自己那份 `case`,那里的 **flush-first 纪律一个字不动**。 */ appendStatus(ev: Parameters[0]): Promise; /** Forward-sink (subagent) events → ledger, bg-leg posture: fire-and-forget with SYNCHRONOUS seq allocation * (sink-vs-loop appends get unique seqs), same whitelist+redact builders (forwarded events are UNTRUSTED * RAW — §E1). Child text/reasoning deltas are deliberately NOT persisted per-chunk (a child's turn text has * no durable turn row; the tool cards + task_progress ticks are the durable projection). */ onForwardEvent(e: { type?: string; }): void; /** Park event (durable F4 suspend / [ref] D-B review pause): ONLY the `checkpointGate` is persisted — * never the capability `checkpointToken` (search [18] Q1b). * 🔴 **The gate is redacted, not "non-secret"** ([ref], 2026-09-10 — the arm [ref] R3 left open): it carries no * capability token, but its `reason` and the `riskDescriptor` leaf family are model-/human-controlled display * text that [ref] measured carrying a PEM private-key body verbatim, and this durable row replays on EVERY * `GET /v1/runs/:id/events` reconnect (core [ref]'s mint-point fix does not backfill rows already written). * The redaction owner is NOT this writer: it is `redactLedgerEventData` — the one confluence every ledger row * crosses at the three store twins' `appendEvent` AND again on the read face (idempotent). Redacting here too * would put two writers on one semantic surface; the other park writers (`http/server.ts`'s two resume legs) * would still need it, and a per-writer arm is exactly the shape that drifts. */ appendParked(kind: ParkLedgerEventType, result: unknown): Promise; /** Terminal `done` event. `result` must already be checkpoint-token-stripped (and, on the bg leg, * model-usage-attached) by the caller. */ appendDone(result: unknown): Promise; /** Convenience for the sync detach lane (no model-usage interleave there): flush, then project the result * to its park/terminal event exactly like the bg leg's done case. `result` must already be stripped. */ onDone(result: { terminal: TerminalCause; }): Promise; } /** * **单写串行链**的可复用形([ref] codex 交叉复审 R1-[high],验真后修)—— `seq` 的**分配发生在链步内**, * 于是「分配序 == 发起序 == 提交序」。 * * 病(实测形,`LedgerSink` 内部早有这条纪律的内联版、其顶注 [1.211 codex H2] 逐字记着它):裸写口 * (`(type, data) => rs.appendEvent(taskId, ++seq, type, data)`)在**同步分配 seq、立刻发起 insert**。 * 主循环 await 自己的写,所以主循环内部有序;但只要有**一个 fire-and-forget 写者**(子代 forward 帧、 * [ref] 的通告口),它的 N 号 insert 可能在池化 SQL 后端上晚于后发的 N+1 号提交。events tail 的读法是 * `getEvents(id, afterSeq)` —— 读到 N+1 就把游标推过 N,**那一行此后永远不会被投递**(重连也不会, * 游标只前进)。行在库里,消费端结构性看不见。 * * 药:所有写口共用一条 promise 链,`++seq` 在链步内取。链上一步失败**不断链**(catch → undefined), * 调用方仍拿到自己那一步的真实结果(await 者照常收到 reject)——与 `LedgerSink` 内联版逐字同语义。 * * ⚠️ 用它的前提是**这条腿的全部写口都走它**(混用一个绕开链的写口 = 病灶原样保留)。 */ export declare function createSerialLedgerAppend(appendEvent: (seq: number, type: LedgerEventType, data: unknown) => Promise, startSeq: number): (type: LedgerEventType, data: unknown) => Promise; export declare function createLedgerSink(opts: { /** The seq-stamped durable append — typically `(seq, type, data) => runStore.appendEvent(taskId, seq, type, data)`. */ appendEvent: (seq: number, type: LedgerEventType, data: unknown) => Promise; /** TRACE_THINKING gate: reasoning deltas are buffered (later redacted+persisted) only when true (core [R57] — * persisting CoT is a deployment decision). Both legs pass the SAME config source (`config.traceThinking`). */ persistThinking: boolean; /** task_notification dedup ledger (core [ref]④a seq-folded keys). The bg leg passes its per-leg instance * (shared with its observer/drain arms so no arm double-appends a key another arm delivered); a leg with no * other durable arms (sync detach lane) omits it and gets a private instance. */ notifiedKeys?: NotifiedKeys; /** [ref] S1:turn 活性打点(`recordTurnActivity(taskId)`)。打点位有两处且都必须:①`onEvent` 入口 * ——**text_delta/reasoning_delta 只进内存缓冲、到 flush 边界才 append**(codex S1-F1,2026-08-13: * 只打 append 会让长时间流式输出被误读成「无进展」,方向与本键的设计目的正好相反);②`append` * 入口——兜住不经 onEvent 的直写路径(appendParked/appendDone/onForwardEvent)。分诊提示性回调: * 抛错不得断链(打点失败丢的只是判别材料,账本不能陪葬)。 * 🔴 [ref] 件3:回调带**事件类型**(`kind`)——`cancelContext.lastEventKind` 的数据源。两个打点位 * 各传自己看得见的那个词(位①=流事件 `ev.type`,位②=账本行 `type`);拿不到类型的打点方 * (core 的 onActivity 第五席、resume 腿手动打点)省参,`kind` 诚实缺席(见 turn-activity.ts)。 */ onActivity?: (kind?: string, statusData?: Record) => void; }): LedgerSink; //# sourceMappingURL=ledger-sink.d.ts.map