import { type RunRecord } from "../plugins/store-contracts.js"; /** 一次终局写口交给观测面的原料(store 侧装配,**未脱敏** —— 脱敏在本模块内做,见 {@link notifyRunTerminal})。 */ export interface RunTerminalRecord { /** 这条 run 的账本 id = `GET /v1/runs/:id` 的 id(本仓 run 行主键名为 taskId,同一个值)。 */ runId: string; /** * 🔴 **只在「这条终局要出声」时在场**(S-450):三只 store 只在 `runTerminalLogLevel(...) !== undefined` * 时才多付那一次 PK 读去装它(成功终局零额外查询 —— 那条注在 `run-store-sql.ts` 的 `setTerminal` 里)。 * 日志腿恰好只在同一形下运行,所以**它读到的恒是在场的**;行为订阅者不得依赖它。 */ sessionId?: string; /** 同 {@link RunTerminalRecord.sessionId}:只在要出声的那一形在场。`null` = 行上没有 owner。 */ owner?: string | null; status: RunRecord["status"]; /** 终局结果里的引擎侧 taskId —— 只在与 {@link RunTerminalRecord.runId} **不同**时进日志(sync 腿上 core 会把它 * 铸成 sessionId,两者同值时重复一遍只是噪音)。 */ resultTaskId?: string | undefined; model?: string | undefined; errorCode?: string | undefined; errorMessage?: string | undefined; /** 行创建到终局的墙钟毫秒(store 侧用行的 createdAt 算;算不出=不传,键缺席)。 */ elapsedMs?: number | undefined; } /** **日志腿**收到的是**已判级 + 已脱敏 + 已截断**的成品——装配层只负责把它送进 logger。 */ export type RunTerminalLogObserver = (level: "error" | "info", fields: Record) => void; /** * **行为订阅者**(S-450):CAS 赢的那一刻收到整只 {@link RunTerminalRecord}。 * * 🔴 载荷是**这次写自己的视角**,不是回读的 durable 行 —— 只有 `runId` / `status` 两位在**任何**形下 * 都为真(CAS 赢 ⇒ 行就是这个词)。日志腿专用的那几位在成功终局上**刻意缺席**(store 不为它们多付一次 * PK 往返,见 {@link RunTerminalRecord.sessionId})。要更多事实的订阅者请自己按 `runId` 读 durable。 */ export type RunTerminalSubscriber = (rec: RunTerminalRecord) => void; /** * 终局行的级别判据。`undefined` = 不打行。 * * 一句话:**非成功终局才出声**(`failed` / `blocked`),用户主动取消降 `info`。词的分类不在本模块做 —— * 它的唯一属主是 `plugins/store-contracts.ts` 的 `RUN_STATUS_CLASS` 表(S-213③:本模块此前手抄了一张 * 穷举 switch,并在 `blocked` 那一格给出与 fleet 读面互斥的答案)。表的编译期双向钉在属主处。 */ export declare function runTerminalLogLevel(status: RunRecord["status"], errorCode: string | undefined): "error" | "info" | undefined; /** **日志腿**的装配点(`boot/stage-01-config.ts` 唯一)。传 `undefined` 卸载(测试隔离)。 */ export declare function setRunTerminalLogObserver(fn: RunTerminalLogObserver | undefined): void; /** * **行为订阅者**的登记口(S-450)。返回解除登记的句柄。 * * 与 {@link setRunTerminalLogObserver} 是**两个语义不同的席**,不是一个带旗的列表: * · 日志腿**恰一只**、装配层亲接、**不隔离**(抛出会让 `setTerminal` reject —— 见 {@link notifyRunTerminal}); * · 行为订阅者**可多只**、运行期来去、**逐只隔离**(抛出吞掉 + `recordFailOpen`)。 * 两条失败契约相反,所以它们必须是两个名字;混进一个列表再加一个 `isolate` 位,就是把「这条腿出事 * 该不该把 run 拖下水」这个**语义**降级成一个**配置**。 */ export declare function subscribeRunTerminal(fn: RunTerminalSubscriber): () => void; /** test-only:清空行为订阅者(用例之间不串台;日志腿用 `setRunTerminalLogObserver(undefined)`)。 */ export declare function resetRunTerminalSubscribersForTest(): void; /** * 🔴 **run 行翻终局的唯一铸点** —— 三个 run store 的 `setTerminal` 在 **CAS 赢了之后**各调它一次 * (SQL 腿 `w.affected > 0`,file / memory 腿在同判据的 CAS 分支内)。「CAS 赢了才发」是**恰一次**的来源: * 调用侧的兜底腿有意会对同一行重复写(cancel × leg-throw × reaper 三向),打空的那几次一个字都不发。 * * 两条出口,**失败契约相反**,顺序承重: * ① **行为订阅者先**({@link subscribeRunTerminal},逐只 try)—— 它们要在日志腿万一违约抛出之前跑完; * ② **日志腿后**({@link setRunTerminalLogObserver},级别筛 + 脱敏 + 不 try)。 * * 为什么日志腿**刻意不 try**(原文保留,[ref] 的成文理由):静默吞掉观测者异常 = 又造一条「走了兜底与 * 一切正常同形」的路,方向与本模块的存在理由相反;生产装配的那一只只调 `Logger`(`createLogger` 的 emit * 内部已把 JSON.stringify 与 write 全部 try 住,结构上不抛)。 * 为什么行为订阅者**必须 try**(S-450 新加的那一半):它们是**行为腿**,不是一只结构上不抛的 logger; * 而本函数跑在 `setTerminal` 的调用栈上、**CAS 已经赢了、行已经落盘**,此时把订阅者的 bug 放大成 * 「终局写 reject」= run 卡 `running` 到 reaper + claim 悬挂([ref] 指纹)。吞但**不静默**: * `recordFailOpen("server.fleet.terminal-notify")`(F 类,普查登记在案)。 * * 脱敏(只在日志腿那一格):`errorMessage` 过 `redactSecrets` 再截断到 {@link MESSAGE_CAP} —— provider 的 * 错误体会原样回显请求片段(取证里的 403 体带订阅链接;别的 provider 会回显 Authorization 头前缀), * 而引擎日志是运维面、常被整条采集走。`errorCode` / `model` 是闭集式短标量,verbatim。 */ export declare function notifyRunTerminal(rec: RunTerminalRecord): void; /** S-105(test [ref] 旁生):终态**持久化**的 `errorMessage` 与日志副本同一脱敏口——provider 错误体(403 体带回显请求头/ * 订阅链接)里的密钥形不得原文落 run 行再由 `GET /v1/runs/:id` 直出。三 run store 的 `setTerminal` 写入前同调此函数; * `undefined` 透传(缺席仍缺席),不截断(日志上限是日志的事,行上保留全文供取证)。 */ /** r4 最终轮 [medium]:`redactSecrets` 会抛 `RangeError`(关键字/前缀臂那一形已由 S-219 的 `atLeast` 铸点闭掉;仍能抛的是 * JSON 秘密字段臂的转义对循环,既有未闭,S-106 登记);S-105 把脱敏 * 挪到 `setTerminal` **入口**后,这个抛会先于 CAS/claim 释放 ⇒ run 卡 `running`、会话 claim 悬挂到 reaper。这里 **fail-closed * 不抛**:脱敏失败 ⇒ 落固定占位 `«redacted:oversized»`(绝不落原文,也绝不阻断终态翻转),并走既有 fail-open 遥测 tag。 */ export declare const OVERSIZED_REDACTION_PLACEHOLDER = "\u00ABredacted:oversized\u00BB"; /** S-107 窄范围重扫 [medium]:`redactSecrets` 对 PEM 起始栅栏密集的多 MB 文案**不抛但慢**(实测 1MB 1.4s / 8MB 7.9s 同步阻塞 * 事件循环),而终态座每次写要扫两遍(appendEvent + setTerminal 入口)、每次读再扫一遍。超过本上限的自由文本**整段**换占位 * (fail-closed:不截断——截断会把密钥切成半截,[ref] 的病;不扫——O(1)),取证面丢的是「>256KB 的错误文案」这一档。 */ /** codex r2 [medium]:上限按**边界最坏耗时**定——PEM 栅栏密集形 256KB 一次扫 0.25–0.46s、终态一次写扫两遍再加读面,一行 * 能让副本卡 ~1s;64KB 时同形 ≤ ~0.1s/次。正常 provider 错误体 < 10KB,>64KB 的错误文案只剩取证价值(整段占位丢弃)。 */ export declare const REDACT_INPUT_CAP = 65536; /** 第十七轮重扫 [medium]:TiDB `task_run.error` 是 TEXT(65,535 **字节**),而上限按**字符**(65,536)量,多字节 / 占位膨胀(`«»` 各 2 字节、 * `«redacted:key»` 比短匹配长)都能让脱后串超列 ⇒ UPDATE 抛 ER_DATA_TOO_LONG、事务回滚、claim 不释放、run 卡 running 到 reaper。 * 脱敏**之后**按字节封顶(占位已替换完,切在占位中间不是泄漏)并带标记。 */ export declare const REDACT_OUTPUT_MAX_BYTES = 65000; export declare function redactErrorMessage(raw: string | undefined): string | undefined; /** * 🔴 **模型自由文本的唯一装载口**(B-106 / S-268,7.75.1)——`TaskResult.result` / `TaskResult.salvagedOutput` * 与账本的 `text` / `reasoning` 两行,四座同调本函数。 * * 它**不是第二只脱敏器**:零自有规则表、零自有词表,判「什么算凭据」的仍然只有 {@link redactSecrets} 一只。 * 本函数只定两件**写回**的事,而这两件事在三条既有纪律面前各只有一个正确答案: * * ① **认出凭据形才改写字节;没认出就回原串**(codex 对抗复审 r1 [medium],亲核实测后修)。 * `redactSecrets` 自 S-103 起**无条件先剥** `\p{Cf}`,所以 `redactSecrets(x) !== x` 对任何含 Cf 的文本都为真 —— * 实测:`👨‍👩‍👧`(ZWJ 家庭 emoji)被拆成三个独立 emoji、波斯语 `می‌خواهم`(ZWNJ)被粘成 `میخواهم`。 * 诊断文本上这是成文代价(那一族本就要「先扫描后渲染」);**用户最终答复**上它是每一条含 ZWJ/ZWNJ 的 * 正常答案被永久改坏,而本件对外的承诺逐字是「不含凭据形的文本逐字节不变」。判据用的是本模块**自己成文的 * 那一条**(`redact.ts` 的 `normalizeForRedaction` 顶注:「正确的谓词是 `redactSecrets(x) !== normalizeForRedaction(x)`」), * 既有消费方 `tool-approval.ts` 的规则车道闸用的也是它 —— 一条谓词两处用,不是新规矩。 * ⚠️ **命中那一次仍然给剥后文本**(S-103 的「先扫描后渲染」不动摇):把零宽字符塞进密钥中间的那条文本, * 渲染的必须是剥后那份,否则消费端把残片拼回原钥。 * ② **扫描器抛 ⇒ 登记过的占位(fail-closed),不把整条 run 拖下水**(codex r1 [high],亲核实测后修)。 * `redactSecrets` 的 JSON 秘密字段臂仍含多字符循环体(S-106 既有未闭:`(?:\\.[^"\\]*)*`),实测 * `{"password":"<400 万个 \\n 转义对>"}`(8MB)抛 `RangeError`。`result` 接进来之前这条路不存在;接进来之后, * 裸调用会让异常穿过 `redactTerminalResult` → 三孪生 `setTerminal`,**一条跑成功的 run 落成 failed + result:null** * (实测读数)。⇒ 与本文件既有的 {@link redactErrorMessage} / `redactDiagnosticText` / `redactArtifactText` * 三座**同一条**处置:抛就换占位、计数、留痕([ref] 的 loud,不是静默 fail-open)。丢的是这一条病态文本, * 不是这条 run。根因(那条正则)是 S-106 的独立车,本函数不在复审轮里改它。 * ③ **不设体量上限**(裁定):`result` 住在 `result` JSON/JSONB 列(亲核 `plugins/tidb-pool.ts` 与 * `plugins/pg-pool.ts` 的 DDL),数 MB 终稿是正常形 —— {@link redactErrorMessage} 的两道上限是为 * `task_run.error` 那一列 `TEXT(65 535 字节)` 定的,套到这里等于用静默的数据损坏换一次脱敏。 */ export declare function redactModelText(raw: string): string; /** {@link redactTerminalResult} 会碰的全部顶层键 = 现行形的 `"redacted"` 行 ∪ 旧形平面键; * `memoRedactTerminalResult` 的记忆键由它派生(两处同源,见 `RESULT_KEY_TREATMENT` 顶注)。 */ export declare const REDACTABLE_RESULT_LEAVES: readonly string[]; export declare function redactTerminalResult(result: T): T; /** S-107(第五轮合并重扫 wf_f00c00bc 两根 [high]):同一个 `TaskResult` 除 run 行外还被逐字追加进 run_events 的 `done` 行 * (bg/verify/cascade/stream/resume 五腿),`failed` 行带原文 `errorMessage`,`GET /v1/runs/:id/events` 原样回放。 * 三孪生 `appendEvent` 入口 + 事件读面(旧行纵深)同调本函数:只认 `done.result`(整块走 `redactTerminalResult`)与 * `failed.errorMessage`,其余事件类型/键逐字不动(它们各有自己的 sink 侧脱敏);形不合(非对象)原样返回,不抛。 */ export declare function redactLedgerEventData(type: string, data: unknown): unknown; /** S-107 第三轮窄范围重扫 [high]×2:leader 面的三处原文座——①`WorkerReport.error`(生产路径经 `runWorker` 构造,`fanout.ts` * 的 runTaskStream 回退座不是生产座);②`LeaderResult.error`(plan/provision 失败带 `e.message`);③`merge.reason` * (coordinator push 失败把 `git clone <带凭据的 durableRemote>` 命令行整段回显)。全部随 leader_run.result JSON 落库并经 * `GET /v1/leader/:id` 直出。修=**咽喉**:fanOut 边界对每份报告脱一次(不问谁产的),endpoint 落库前 + 读面各脱一次 * (幂等;旧行纵深)。只碰这三个自由文本键,其余逐字。 */ export declare function redactWorkerReportError(report: T): T; export declare function redactRiskDescriptor(rd: unknown): unknown; /** `CheckpointGate`(core 类型)整块上 wire —— `reason` 与 `riskDescriptor` 的叶族是自由文本 * ([ref] 实测 summary 带 PEM 明文;[ref] R3 登记;[ref] 把它扩到结果/账本两面)。逐键**按表**处置, * 叶表属主见 {@link redactRiskDescriptor}。非对象形原样返回(同一引用);字节没变也回同一引用。 */ export declare function redactCheckpointGate(gate: unknown): unknown; /** 工件字节(patch / *.rej 拒绝块):codex r6 [high]——拒绝块能带 `git clone https://u:TOKEN@host` 或 `_authToken=` 行, * 「工件保真」不能让原凭据上元数据读面。做**原位密钥形替换**(与预览同一张规则表,字节只在密钥处变)、**不截断**; * 单份上限 256KB(`ARTIFACT_INPUT_CAP`,第八轮重扫:不封顶=worker 可控 DoS)——超过整份换占位,这是有意的取证丢弃 * (原字节仍在 worker 分支 / merge 咽喉);扫描器抛 ⇒ 占位(fail-closed)。server 无任何消费方用持久化的这份字节回放。 */ /** 诊断文本(stdout/stderr/err.message/模型输出)的「先脱后切」——第九轮重扫 [medium]×3 + 第十轮 [high]×2 + codex r9 [high]: * ①不能把整段喂进 64KB 上限臂(长测试日志整段占位、灌水计数);②**固定 PAD 的窗不是 fail-closed**:比 PAD 长的密钥形 * (>4096 的 URL 密码 / JSON 值 / PEM 体)锚点落在窗外,窗内只剩无锚的体,任何规则都认不出;窗在脱敏下缩短时残片还会 * 落进最终切片。所以**不开窗**:文本 ≤ 256KB ⇒ 整段扫(guard,不封顶于 64KB;PEM 密集形最坏 ~0.4s/座)再切;> 256KB ⇒ * 整段换占位并带原长(fail-closed 取证丢弃,单独计数:普查第 59 行)。 */ export declare const DIAG_INPUT_CAP = 262144; export declare const OVERSIZED_DIAG_PLACEHOLDER = "\u00ABredacted:oversized-diagnostic\u00BB"; export declare function redactDiagnosticText(text: string): string; export declare function redactHead(text: string, keep: number): string; export declare function redactTail(text: string, keep: number): string; export declare const ARTIFACT_INPUT_CAP = 262144; export declare const OVERSIZED_ARTIFACT_PLACEHOLDER = "\u00ABredacted:oversized-artifact\u00BB"; export declare function redactArtifactText(raw: string): string; export declare function redactLeaderResult(result: unknown): unknown; /** 同上,附带「有没有任何叶子变过」——读面记忆用它区分「本就干净的行」(只记 id,不留体)与「旧脏行」(留脱后体)。 */ export declare function redactLeaderResultWithFlag(result: unknown): { value: unknown; changed: boolean; }; export declare function memoRedactedRead(seat: string, raw: T, redact: (v: T) => T): T; /** run 终态 result 的读面记忆只按**可脱敏的那几叶**({@link REDACTABLE_RESULT_LEAVES},属主是 * `RESULT_KEY_TREATMENT`)做键与留体—— * 第十六轮重扫:按整块 result 摘要每次轮询要 stringify 一份可能数 MB 的 result,且大 result 的脏行永远存不进去。叶全缺席 * ⇒ O(1) 直返(`redactTerminalResult` 本就同引用)。 * 🔴 S-136(core 7.6.0 D-8):新形把前两片搬进了 `terminal`(同属一个键,按它整只做叶更省);旧形的两片 * **照留**——盘上真有那样的行,而读面纵深正是为它们存在的(codex 对抗复审 r1 [high] 验真后补)。 * 🔴 **B-106 起「叶全缺席」这条捷径实际上不再触发**(如实写明,不是漏改):`result` 在 core 类型上是 * **必填**串,进了叶集就恒在场 ⇒ 每次读面都要对叶子做一次 sha256。代价是有意接受的,因为省下的那条 * 捷径对**含自由文本的 run(即全部 run)**本来就不成立 —— 不把 `result` 放进叶集才是真的漏: * `memoRedactTerminalResult` 会对它 O(1) 直返明文,读面纵深对这一叶恒不生效(这正是 [ref] 立案时 * `editedFiles` 那一格的病)。读数:8MB result 的一次摘要 ~10ms 量级,远小于一次全扫;干净行仍只摘要 * 一次即进 clean 集,脏行超过 {@link READ_MEMO_DIRTY_MAX_ENTRY_BYTES} 时退化为每次重扫(既有形)。 */ export declare function memoRedactTerminalResult[0]>(seat: string, result: T): T; export declare function diffScanShadowCountForTest(): number; export declare function resetDiffScanShadowCountForTest(): void; export declare function resetReadRedactionMemoForTest(): void; //# sourceMappingURL=run-terminal.d.ts.map