/** * 上 wire 的通告码 —— **显式闭集常量表**(设计稿 ①,起步三码;今天的码数以本常量自身为准—— * 在散文里抄数字必陈腐,codex 5.65 增量批 R1-[low] 实逮:上一句曾写「今天九码」而表里是十条。 * 码数的**受门**真源=契约文档附录 D.3 的小节标题,⑥b 双向门盯着)。 * * 起步的那六码同族:都是「这条 session 的记忆姿态」的披露,resume 之后仍然相关 ⇒ 也进 durable 账本 * (cli 断连补看走既有 events 重放腿,[ref](ii))。 * * ⚠️ **自 [ref](core 5.61.0)起本表不再是单一族**:`task.turn_interrupted` 与 `task.user_*_undrained` * 两族是前三条**非记忆面**的成员(前者=一次插话真的切断了在飞的 turn;后两者=用户的输入被彻底丢弃)。 * 判据没有变(受众 × 投递可行性 × 去重单位三条合取,见顶注),变的只是「碰巧六条都是记忆码」这个 * 事实——别再把「是不是记忆姿态码」当成入册的隐性第四条判据。这条隐性判据正是 `task.user_*_undrained` * 两码在明拒表里陈腐多时的帮凶之一(见它们的键注)。 * * 🔴 **表长了就得同步三处**([ref] 实测,少一处即门红):`test/engine-notice-wire.test.ts` 的闭集 pin、 * 同文件 G1 e2e 的全码扇出夹具、以及契约文档附录 D.3 那张表(含**小节标题里的码数** —— 那道门本批 * 刚立,立完就在下一次加码时自己咬住了)。 */ export declare const ENGINE_NOTICE_WIRE_CODES: readonly ["memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "task.turn_interrupted", "steering.parked_input_blocked", "task.user_steer_undrained", "task.user_followup_undrained", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "task.halt_unconsumed", "task.late_approval", "config.durable_gate_unavailable", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "mcp.injection_dropped", "delegation.ask_unresolvable", "task.interrupt_unconsumed"]; export type EngineNoticeWireCode = (typeof ENGINE_NOTICE_WIRE_CODES)[number]; /** 白名单谓词(单点):路由与门都读这一个,不许第二处手抄码串。 */ export declare function isEngineNoticeWireCode(code: string): code is EngineNoticeWireCode; /** * 归属键抽取 —— `detail.sessionId` 提升为顶层(设计稿 ②:归属键一等化)。 * * 非串 / 空串一律按**缺席**处理(不铸 `"undefined"` 这种看起来合法的假值——`compactionOutcomeEventData` * 的同款缺键守卫纪律)。core [ref] 的顶层 `EngineNotice.sessionId` 到货后,这里先读顶层再回落 detail * (两处同值,additive 零 BREAKING),届时本函数是唯一改动点。 */ export declare function engineNoticeSessionId(notice: { sessionId?: unknown; detail?: Record; }): string | undefined; /** wire 上的 `engine_notice` 载荷(**五键固定**,设计稿 ②)。`type` 由发帧处补(SSE)/ 由账本行的 type 列承载。 */ export interface EngineNoticeWireRow { /** 稳定机器码 —— 消费契约:cli 按 `code`+`detail` 渲染(见 {@link ENGINE_NOTICE_WIRE_CODES} 各码的 detail 形)。 */ code: string; /** * core 铸的人话行。🔴 **仅 fallback 展示,不是匹配键**:core 明写 `memory.session_polluted` 的 message * 随 `memoryProvenance` 模式变文([ref] mode-aware),按 message 匹配必碎(5.41 合流码形退役同教训)。 */ message: string; /** 机器可读事实,逐键消毒后直透(见 {@link sanitizeNoticeDetail} 的消毒/尺寸契约)。 */ detail: Record; /** 归属键(从 detail 提升)。本行只会出现在**这一条** session 的流上。 */ sessionId: string; /** **服务端观察时刻**(ms epoch)——`EngineNotice` 自身不带时间戳,这是本仓收到它的那一拍,不是 core 铸它的那一拍。 */ ts: number; } /** * `detail` 消毒(设计稿 ②:`redactSecrets` + 尺寸 bound;`workspaceChangedEventData` 先例)。 * * core 已 neutralize 三码的 `reason`(工具名/子代类型是 host/模型可控输入),**server 按纪律再过一遍** * ——§E1 的既有姿态:上 wire 的自由文本一律经本仓的脱敏器,不因为上游说过一次就免检。 * * 契约(逐条,门在 `test/engine-notice-wire.test.ts`): * · 串 ⇒ `redactSecrets` + 截到 {@link NOTICE_TEXT_MAX}; * · 有限 number / boolean / null ⇒ 逐字(harvest 的 count/moved/escalated 是判据数字,不许变形); * · 非有限 number(NaN/Infinity)⇒ **丢键**(JSON 里它们会变成 `null`,一个看起来合法的假读数); * · 数组 ⇒ 逐项消毒,超 {@link DETAIL_MAX_ITEMS} 截断;对象 ⇒ 逐键消毒,超 {@link DETAIL_MAX_KEYS} 丢尾键; * · 深度超 {@link DETAIL_MAX_DEPTH} ⇒ 该子树丢弃; * · 其余(function / symbol / bigint / undefined)⇒ 丢键(JSON 不可承载或语义不明)。 */ export declare function sanitizeNoticeDetail(detail: Record | undefined): Record; /** * `engine_notice` 载荷构造器(三条终点腿同源:live SSE / durable 账本 / 未来的任何第四条腿)。 * * 与 `trace/project.ts` 的 `*EventData` 族同纪律(白名单挑键、禁 `{...notice}`),但**不住在那个文件里**: * 那一族的入参全是 core `TaskEvent` 的臂(有 `wire-whitelist-exhaustiveness` 的臂集编译门看着), * 而 `EngineNotice` 不是 `TaskEvent` —— 混进去会让那道门的语义变成"两个不同来源的联合",判别力反而降。 */ export declare function engineNoticeEventData(notice: { code: string; message: string; detail?: Record; }, sessionId: string, nowMs: number): EngineNoticeWireRow; /** 一条 run 腿注册进来的投递口。抛错由 {@link EngineNoticeRouter.route} 隔离(见那里的 fail-open 登记)。 */ export type EngineNoticeWireSink = (row: EngineNoticeWireRow) => void; /** * sessionId → 该会话活跃 run 腿的投递口。**进程内**(`defaultSubagentTailBus` 同款姿势): * `onNotice` 是同进程的同步回调,通告与它要投的那条腿必然同副本,跨副本路由这件事结构上不存在。 * * 🔴 跨 session 隔离靠的是 **Map 键的精确串相等**,不是任何「最近活跃」回落 —— 缺席就是缺席(G5)。 * * 🔴 **同 session 的跨代隔离靠 legId「最新代独占」**([ref] codex 交叉复审 R1-[high],验真后修)。 * 病(真窗,本仓每一处 in-flight 登记都为它写过 identity-guard):run 行在**流体内**就已 setTerminal / * setSuspended,而本腿的注销在 `finally` —— 中间那一窗里,同一条 session 上的**下一条腿**(快速续跑 / * 紧接着的新提交)已经注册好了自己的口。若按「会话内全量扇出」投,新腿的一条通告会**同时**落进: * · 旧腿那条已 end 的 SSE(顶多记一次 fail-open,无害),以及 * · **旧 taskId 的账本** —— 一条不属于那条 run 的、终态之后的行;resume 复用同 taskId 时还会与新腿 * 抢同一个 `(task_id, seq)` 域。这不是跨租户外泄,是**同租户跨 run 的账本污染**,同样必须关死。 * 药:每次 `registerLeg` 铸一个单调 legId,`route` **只投最新代**;旧代的口留在表里(它自己的注销仍 * 按身份精确删,不会误删新代),但一帧都收不到。方向 = fail-closed(宁可旧腿少一帧,不可新腿的事实 * 写进旧腿的账)。 */ export declare class EngineNoticeRouter { private readonly legs; /** * 登记**一条腿**的全部口(live / durable 作为**一代**同进同出)。返回注销函数(幂等,身份精确删)。 * * 每只口各自 try 隔离在 {@link route} 里,单口抛不牵连同腿另一口。 */ registerLeg(sessionId: string, sinks: readonly EngineNoticeWireSink[]): () => void; /** 单口腿的便捷形(测试与单终点腿用)。 */ register(sessionId: string, sink: EngineNoticeWireSink): () => void; /** 诊断/门用:某会话此刻**最新代**有几只口在听(旧代不计——它们收不到帧)。 */ sinkCount(sessionId: string): number; /** 诊断/门用:某会话此刻有几代腿在表里(>1 = 正处在换代窗内)。 */ legCount(sessionId: string): number; /** * 分流器的 wire 半场。返回**真投出去的口数**(0 = 没投:非白名单码 / 缺 sessionId / 该会话无活跃腿)。 * * 单只口抛错**不吃掉同腿的其它口**(fleet bus 订阅者隔离同理由),并按 F 类登记留痕。 */ route(notice: { code: string; message: string; detail?: Record; sessionId?: unknown; }, nowMs?: number): number; } /** 进程单例(`defaultSubagentTailBus` 同款):分流器与 run 腿直接 import,不穿长参数列表;测试自 new 独立实例。 */ export declare const defaultEngineNoticeRouter: EngineNoticeRouter; /** * run 腿的注册助手 —— 三条腿(sync stream / bg / resume-verify)用**同一个**形,免得各写一份漂。 * * `sessionId` 缺席(adhoc 无会话腿)⇒ 返回 no-op 注销函数,一个字节不注册(顶注的 fail-closed 方向)。 * `live` / `durable` 各自可缺席:bg 腿没有 live SSE(消费方走 events tail),ephemeral sync 腿没有账本。 * * 🔴 两口注册成**两只独立 sink**(不是一只闭包里顺序调两次):route 的隔离粒度是 sink,合成一只会让 * live 写(断连的 socket、JSON.stringify 抛)吃掉同一拍的 durable 落账 —— 而 durable 那半正是断连之后 * 唯一还看得见这条通告的地方(G3),两者的失败必须互不牵连。 */ export declare function registerEngineNoticeLeg(opts: { sessionId: string | undefined; live?: EngineNoticeWireSink; durable?: EngineNoticeWireSink; router?: EngineNoticeRouter; /** * L-167 —— **腿开张之前**就已经铸好的通告(装配期 `resolveSpec` 的产物,经 * `PreparedTaskSubmission.engineNotices` 递过来)。注册完口**立刻**经同一只 {@link EngineNoticeRouter.route} * 投出:白名单、归属键、消毒、隔离四件判据一个都不绕,**不是**第二条投递机制 —— 只是把 `route()` 挪到 * 「这条会话已经有口在听」的那一拍。 * * 🔴 为什么不让铸点自己 `route()`:装配(`prepareSpec`)跑在本函数**之前**,那一刻这条会话一只口都没有, * `route()` 恒返 0 —— 一条结构上永远投不出去的用户面披露,就是本仓 `task.user_steer_undrained` 陈腐明拒 * 那一族的病。缺席/空数组 ⇒ 一个字节不投(既有腿逐字不变)。 */ pending?: readonly { code: string; message: string; detail?: Record; sessionId?: unknown; }[]; }): () => void; //# sourceMappingURL=engine-notice-wire.d.ts.map