import type { TaskNotificationPayload, TerminalCause, WorkflowRun } from "@sema-agent/core"; import type { RunRecord, UnsuccessfulRunStatus } from "../plugins/store-contracts.js"; import type { AssertAllKeysHandled } from "../trace/core-keyset-guard.js"; /** [ref] 幽灵行案的单源判别:一条 `task_notification` 只有在 **agent 族 × 终态** 时才允许打 * `onChildTerminal`(fleet「subagent 树」只渲 agent 子代)。`background_bash`/`monitor`/`external` * 不属 agent fleet 树——它们此前每条都打,fleet-bus 的「无 tick 无 claim」臂给 b\* 与 m\* handle 合成 * agent 形瞬态终帧,壳终态留存池把这帧渲 60s = footer 幽灵子行;monitor 的 `status:"event"` 还被 * 终态映射折成 `failed`。 * * 🔴 [ref]([ref]):判别**本体已换 core 的 {@link isDelegatedAgentTerminal}**。此前这里是一份手写语义 * 孪生(自己枚举 `completed|failed|killed|cancelled`),头注自陈「与 core 语义同源——core 根导出后换装 * import 删本镜像」。core 现已从包根导出(`index.d.ts`),且它的终态集与 pending 店的驱逐偏好共用 * 同一份 `TERMINAL_STATUSES`(core 原话:「so "terminal" can never mean two things in this module」)—— * 孪生留在这里等于让 fleet 面板对「终态」的理解可以和引擎分家。本函数保留为**具名的 fleet 语义位** * (调用点读的是「这条通知该不该打 fleet 的 onChildTerminal」,不是「core 怎么判终态」)。 */ export declare function isFleetAgentTerminalNotification(n: Pick): boolean; /** The fleet vocabulary the shell renders (MF-Fleet `FleetTask.status`). The service maps its run/workflow status * onto this neutral set; `awaiting approval` = a needs-review/plan-approval park, `waiting` = a durable suspend. * * §K-7 (shell ASK 2026-06-28) asked for a `report_blocked` / `nesting` status extension to hide blocked internal- * nesting agents from the picker. DELIBERATELY NOT EXTENDED — the need is already met without breaking the * SDK byte-alignment (`@sema-agent/sdk` 0.0.39 keeps this exact 9-value set, and the SDK explicitly declined to add * one unilaterally, awaiting a service decision): (1) `report_blocked` = a TERMINAL outcome (the agent gave up) → * `runStatusToFleet("blocked")→"failed"` → the row is REMOVED (onTerminal), so a blocked agent never lingers in * the picker (the leaked-stale-blocked-row bug that triggered the ask was fixed in 1.11.0); (2) "internal nesting" * is already expressible — every subagent CHILD row carries `parentId`, so the picker filters its flat view to * top-level rows (`parentId` absent) and shows the rest only in the tree. Adding a redundant status/flag that * carries no non-derivable information would only create wire surface the SDK must mirror. The real picker fix is * the additive `agentType` carrier above (a short type label instead of the objective). */ export type FleetTaskStatus = "queued" | "running" | "waiting" | "stopping" | "awaiting approval" | "idle" | "completed" | "failed" | "killed"; /** One active task row (wire subset of the contract `FleetTask` — `depth`/`viewed`/tree-`selection` are * shell-local render state the renderer composes; the wire carries the data that TRANSITIONS). */ export interface FleetTaskRow { id: string; /** 显示名。**缺席不铸**([ref]③ cli 定谳,server 3.5.1):没有真名时本键**不发**。 * ⚠️ 两代语义留档 —— 3.5.0 及以前,三处在无真名时把**行 id** 顶到本位(匿名子代 tick 铸子代 uuid、 * `onChildTerminal` 合成终帧铸 a\* handle、`publishTask` 懒铸底座铸行 id),于是人眼看到一串乱码 * (clay 亲报的「子代名乱码」形),消费端也**分不清**「它就叫这个」与「我们不知道它叫什么」。 * 3.5.1 起后者 = 键缺席,显示占位归客户端渲(cli 已收敛 `'(unnamed)'`)。同一条纪律见 [ref] * costUSD:诚实缺席 > 编造。(SDK `FleetTaskRow.name` 同步放宽 —— 见发车工单。) */ name?: string; description?: string; /** §K-7 (shell ASK 2026-06-28; byte-aligned to `@sema-agent/sdk` 0.0.39 `FleetTaskRow.agentType`/`agentName`) — a * SHORT agent-TYPE label so the picker renders like CC 187 (`Explore`/`general-purpose`/`code-review`) instead of * the full objective. `agentType` = the "what kind of agent" (a subagent's type / a delegated `taskName`); * `agentName` = an in-process teammate's INSTANCE name. ADDITIVE / tolerate-absent: a TOP-LEVEL run carries NO * native agentType (sema design — top-level runs have no agent-type; the analog is the SCENARIO, whose surfacing * is the still-open "fleet-row name source" decision for clay/shell) → ABSENT, and the consumer FALLS BACK to * `name` (the objective-preview). Populated TODAY on SUBAGENT CHILD rows, where `task_progress.name` IS the type * (BC-2). SECRET-SAFE: redacted before emit, same discipline as `name`. */ agentType?: string; /** 队友的**实例名** = `Agent({name})` 的显式出生名。源 = core 7.25.0 的 `BackgroundChildEvent.spawnName` * ([ref] S0;spawn/tick/terminal 三帧都带,出生点按 `^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$` 校验过、永不 `main`)。 * 🔴 与 `name` 分域:`name` 是**显示名**(源 `description ?? name`,模型自由文本);本键是**身份键**, * 消费端按串相等把「@某个名字」绑到这一行(同类型两个子代在 `agentType` 上不可分,在本键上可分)。 * ADDITIVE / tolerate-absent:**缺席 = 一次匿名委派**(不是「不知道它叫什么」)—— 7.25.0 之前本键在本仓 * 没有任何源,恒缺席。SECRET-SAFE:与 `name` 同一道边界(emit 前 redact)。 */ agentName?: string; /** Owning principal (= the run's scope). The `/v1/fleet/stream` route filters to the caller's scope (owner-gated) * unless the caller is fleet-wide (operator / trace-token). NOT rendered — a tenant-isolation field. */ scope?: string; /** Fleet per-session scope: the run's rootTaskId (= sessionId). The * `/v1/fleet/stream` route ALSO filters to the caller's `?session=` when present, so a TUI footer shows only the * CURRENT session's rows, not OTHER sessions of the same principal (the "他会话 token" leak). NOT rendered — a * scoping field like `scope`. Absent ⇒ the row is visible to any same-principal caller (back-compat, unscoped). */ sessionId?: string; parentId?: string; /** [ref] P1-2(core 1.432 BackgroundChildEvent.parentToolCallId,「出生就在场」语义):发起本子代的 * 父 tool_use id **原值**(E2 域,core-mint 不含用户内容)——与 `parentId`(解析后的行链接)是两个域: * 本键给按 tool_use id 建卡树的消费端(cli 卡片树)直接绑定,无需 FIFO 猜/形状嗅探。 * ADDITIVE / tolerate-absent(旧 core 不发)。 */ parentToolCallId?: string; /** [ref] [ref]:wa\* 行的 workflow 归属附注位(w\* runId)——面板按此分组;footer 树不按此嵌套 * ([ref] CC 锚:wa\* 不进 footer,壳按 taskId 前缀过滤)。ADDITIVE / tolerate-absent。 */ workflowRunId?: string; status: FleetTaskStatus; /** core 1.367 δ([ref]/[ref]④):launch 时刻 epoch ms(revive 行=本 revive 周期 wake 时刻)—— * cli 的 elapsed 锚直出(比 server 端 elapsedMs 快照更稳,消费方自算不漂)。ADDITIVE / tolerate-absent * (a* 车道 spawn 即带;诚实缺席——回落 server 收帧时刻的臂已随 [ref] 删,floor ^4.0.0 下 startedAt 恒在)。 */ startedAt?: number; elapsedMs?: number; tokens?: number; awaitingPlanApproval?: boolean; /** core 1.262.0: the child's most recent tool intent as one * human line ("Bash npm test") — from the bg sink tick's `currentAction` (core's only attachment point * today). UNTRUSTED (tool argument head) → redacted before publish, same discipline as `name`. * ADDITIVE / tolerate-absent. */ currentAction?: string; /** cli [ref]②(core 1.429 `currentTool`):`currentAction` 的**结构化**同源体 —— CC220 详情页 * Progress 段消费 `{toolName, target}` 结构(toolName 查工具注册表,自由文本塞不进去)。 * `toolName` = core 工具名 verbatim;`target` = UNTRUSTED 自由文本(命令行/路径可能秘密形)→ * redactSecrets + bound,与 `currentAction` 同信任边界。与 currentAction 同生命周期(未跑过工具前 * 缺席);ADDITIVE / tolerate-absent(旧 core 不发 ⇒ 键缺席,消费方回落 currentAction 文本)。 */ currentTool?: { toolName: string; target?: string; }; /** cli [ref]①:该子代**累计**的工具调用次数(bg sink tick 的 `usage.toolUses`)——面板行与详情页的 * tool 计数。⚠️ **累计值,直接上行不累加**(与 `turn_end` 的每轮增量相反);⚠️ **缺席 ≠ 0** —— * 老 core 不发这个数时本位缺席,消费方不得当 0 渲染(那会把「不知道」伪装成「跑了 0 个工具」)。 * ADDITIVE / tolerate-absent。 */ toolUses?: number; /** cli [ref]②:该子代的转录 id —— 壳侧据此去转录里取**委派 prompt**(详情页 `Prompt` 段)。 * ⚠️ 为什么给的是映射而不是 prompt 本身:`BackgroundChildEvent` 上**没有 prompt**(core `types.d.ts`), * server 收不到的东西不能凭空造 —— 要 prompt 直出需要 core 先发。(旧注还说「也无 parentToolCallId」, * core 1.432 已补该键,见 FleetTaskRow.parentToolCallId——本键的理由只剩 prompt 一半。) * ADDITIVE / tolerate-absent。 */ transcriptId?: string; /** [ref]①/(a) 案:**终态行帧的残局键**——cli 实测裸 durable-run 完成时终态 task 帧键集只有 * [id,name,status,tokens,elapsedMs,description],idle 观察者(web 首屏形)从行卡拿不到终局。 * (b) 案(为顶层 run 造 bg_notification)被否:那族语义=观察者眼里的**子代**,自造事件族比丰富行帧 * 更伤([ref]§二)。四键全部 ADDITIVE / tolerate-absent、**只在终态帧出现**: * - `stoppedBy`:core open enum verbatim(与 BgNotification 同姿势);顶层 run 仅 cancel 路径有源 * (= "user",与 markStopSource 同源值),其余终态诚实缺席。 * - `usage`:与 {@link BgNotification.usage} **同形**(消费端一个形状)——bg 子代=terminal 事件的 * usage 逐键挑;顶层 run=TaskResult.stats 映射(tokens→totalTokens、toolCalls→toolUses)。 * - `resumable`/`editedFiles`:**仅 bg 子代有源**(BackgroundChildEvent.terminal);顶层 TaskResult * 上没有这两位 ⇒ 顶层行帧永缺席,不造([ref]§二「没有就诚实缺席」)。 */ stoppedBy?: string; usage?: { totalTokens?: number; toolUses?: number; durationMs?: number; tokens?: number; turns?: number; costMicroUsd?: number; }; resumable?: boolean; editedFiles?: Array<{ path: string; edits: number; }>; /** [ref]①/[ref] 裁定(server 半场,3.6.0):**产出这一行的车道**。 * - 缺席 = BCE 车道(`fleetBackgroundChildPublisher`)的 a\* 与 wa\* 真行,或顶层 run 行 —— 单源,常态。 * - `"run-leg"` = 由每腿的 `fleetRunPublisher` 从 `task_progress` tick 铸的**复合 id 行** * (`${runId} ${childTaskId}`)。此形只在**没有 BCE 真行**的前台 delegation 上保留(同步腿委派的 * 子代不过 background-child registry ⇒ 不让位就没有行);同 (scope, progressTaskId) 一旦有 BCE 真行, * 本车道**让位不铸**(双生行退役)。 * 消费端读法:带 `sourceLane:"run-leg"` 的行是过渡形,与 a\* 行**不会**同时描述同一个 agent;不需要再 * 自己发明「复合 id 首段==parentId 且撞 (parentId,name)」那类集合级判据(cli 侧的防御税)。 * ADDITIVE / tolerate-absent。 */ sourceLane?: "run-leg"; /** * [ref] §2①(core 5.36.0 [ref] 供给,server 白名单透传):这一行的**代际号**。语义 = core 的 * stop-cycle 计数(fresh spawn IS cycle 1,每次复活翻转 +1),与 `bg_notification.seq` / * `task_progress.seq` / `BackgroundChildEvent.seq` **同域同轴**(core 原话:一条轴,不是第三种拼法)。 * * 消费端读法(定死,防两端各写):同 id 帧 `cycleSeq` **更大 ⇒ 复活**(新代际,行内累计量重置); * **更小 ⇒ 前代迟到帧,忽略**(不回退——本仓在 {@link FleetEventBus.publishTask} 里 merge 前就判掉了, * 消费端拿到的流已经是单调的);**缺席 ⇒ 回落现行为**(tolerate-absent)。 * * 🔴 缺席是**事实**不是缺口(core [ref] 逐字定谳,以其措辞为准):没有 `a*` registry 行的 run * ——同步委派子代、workflow `wa*` agent、**顶层 run 行**——根本没有代际概念,「缺席即『无此概念』, * 非『第一代』」。任何把缺席读成 1 的消费端都会把「首帧迟到」误判成「复活」。 * ADDITIVE / tolerate-absent。 */ cycleSeq?: number; /** * [ref] §2②:这一帧的终态**不是发布方亲报**,而是对账腿({@link ../fleet/fleet-reconciler.js}) * 从 durable run 行**投影**出来的 —— 发布方死了(事件环异常中断 / 引擎被壳 reuse 而旧 run 对象已死 / * BCE 终态通知丢失),行本会永久僵在 `tasks` Map 里当幽灵。 * * 词表现为**单词闭集**;读侧按 SDK 开集读纪律(未来若有第二个投影者,它会是一个新词而不是改义)。 * 发布方亲报的终态帧**恒不带**此键 —— 两种终态在 wire 上因此**可判**(消费端要区分「引擎说它完了」 * 与「我们从库里读出来它完了」时,这是唯一的判据)。只在终态帧出现。 * ADDITIVE / tolerate-absent。 */ retiredBy?: "reconcile"; } /** One workflow row (wire subset of contract `FleetWorkflow`). */ export interface FleetWorkflowRow { id: string; /** [ref] FLEET-WORKFLOWS-1([ref]③ 同款,workflow 行此前漏网):缺席=诚实缺席,**不铸 id 冒充名**。 * 现役唯一发布者(workflow-notify-journal)恒带 redact 后的真名;此可选位只为乱序/部分 delta 诚实。 */ name?: string; description?: string; /** Owning principal (= the workflow run's scope). Same owner-gate as FleetTaskRow. NOT rendered. */ scope?: string; /** #3 fleet per-session scope (rootTaskId = sessionId): the `/v1/fleet/stream` `?session=` filter. NOT rendered. */ sessionId?: string; status: string; doneCount?: number; /** 计划总数(= `run.agents.length`,**含排队中**)—— 注意它**不是**"已启动数",见 {@link FleetWorkflowRow.startedCount}。 */ totalCount?: number; failedCount?: number; /** MF-Fleet additive(cli [ref] 二①):**已启动**(非排队中)的 agent 数 —— CC 2.1.220 `⚠ Large workflow` * 规模告警的分母。判别用 core 导出的 `deriveAgentDisplayStatus`(`startedAt` 未置 ⇒ 显示态 `queued`), * **不自己重写谓词**:两端各写一遍必然漂移(本仓刚在治理面镜像上吃过同款)。 * ADDITIVE / tolerate-absent:缺席时消费方可退回 `doneCount + failedCount` 这个**诚实下界**(告警偏早)。 * 2.1.250 复核([ref],语料=cc-decoded/cli250.js):告警函数 220↔250 逐字同形——agents 轴比的是 * `scheduledAgents`,`started` 是 token/agent **投影的分母**(`started>0 ? totalTokens/started : 70k`); * 阈值 25/1.5e6/70k 与 `CLAUDE_CODE_WORKFLOW_SIZE_WARNING_*` 覆盖名两版一致。 */ startedCount?: number; elapsedMs?: number; tokens?: number; } /** * durable {@link WorkflowRun} → 一条 fleet workflow 行的**纯投影**(`build*`=纯数据,无行为、不发布)。 * * 从 `JournalingWorkflowRunStore.publishFleet` **原样**抽出,因为它现在有两个调用点,而两处各写一遍 * 必然漂移(本仓在 startedCount 口径上已吃过同款): * ① 活写路径 —— put/update 的写观察点(行的唯一写者,见该类注); * ② `/v1/fleet/stream` 连接时快照的**有界终态行窗**([ref] 修方向 1)—— 引擎重启后 boot 扫描把前世 * running run 判死,终帧与撤行在同一同步栈内背靠背发出,之后才连上的客户端连快照都看不见那一行。 */ export declare function buildFleetWorkflowRow(id: string, run: WorkflowRun): FleetWorkflowRow; /** A push frame on the fleet stream. `snapshot` = the full current state on connect; `task`/`workflow` = a single * row upsert (the row transitioned); `task_remove`/`workflow_remove` = the row left the active set (terminal + * swept). Stable identity (`id`) lets the UI update/remove the right row in place. */ export type FleetFrame = { type: "snapshot"; tasks: FleetTaskRow[]; workflows: FleetWorkflowRow[]; ts: number; } | { type: "task"; row: FleetTaskRow; ts: number; } | { type: "task_remove"; id: string; ts: number; /** * [ref] §2③:**陈述级退场** —— 行没了,但**没有**结算语义。 * * 🔴 **本键缺席的那一族不是「终局已发过」**(S-449 / 车HX codex r1 [medium];本注此前逐字写着 * 「无此键的 remove = 终态之后的常规清场……两者都先发过终态 `task` 帧」,**那句是假的** —— * 车HW 逐条亲读本文件的七处 `removeTask` 铸点后改成真话,消费方面的成文版 = * `docs/ASSISTANT-WIRE-CONTRACT.md` 附录 H)。缺席形其实是**两族**: * (**按臂名找,不给行号** —— 本注与铸点同住一个文件,写死数字会被下一次改注自己顶歪。) * · **结算级** —— 这一行**自己的**终态 `task` 帧刚发过,remove 只是成对的后半。四处: * `fleetRunPublisher.onChildTerminal` 的「bash 无 tick 形」合成瞬态帧(publish → remove 同一个 * `for` 体)、`fleetRunPublisher.onTerminal` 的顶层行(`publishTask(run.runId, fleetStatus)` → * `if (terminal) removeTask(run.runId)`)、`fleetBackgroundChildPublisher` 的子代终局 * (带残局键的行帧 → `removeTask(e.taskId)`)、以及对账腿 `fleet-reconciler.ts` 的 `retire` 与 * 候补窗 `upgradeWatched`(两者都先帧后撤)。 * · **投影级 —— 没有自己的终态帧,但也不是「这个任务结束了」**:① **双生让位 / 双生抑制** * (`yieldToBackgroundChildLane` 把 run-leg 行让给 BCE 权威行;tick 期与子代终局期两处按 * ` ${uuid}` 后缀扫掉 run-leg 复合键双生)—— 同一个实体改由**另一行**渲染,终局会落在**那一行**上; * ② **父 leg settle 时把子代行收走**(`onTerminal` 里的 `for (const cid of children) removeTask(cid)`), * **含父 parked 的形**(理由逐字在该处:子代在父 park 期间并不在跑,resume 后新的 `task_progress` * 会重新建行;而子代本来就从不发终态 tick)。 * ⇒ 对消费端的纪律只有一条:**`task_remove` 在任何形态下都只说「这一行从本副本的面板上离场了」, * 不说结算**。唯一的终局证据是那一行**自己的**终态 `task` 帧(`status` ∈ completed/failed/killed), * 或对 durable 面的权威查询(`GET /v1/runs/:id`)。 * * 铸点只有对账腿:durable 说这条 run 还 `running`(reaper 宽限窗内)、或 claim 在他副本、或 * durable 行压根不存在 —— 三形共同点是**没有终局可投影**,而本副本的行必然陈旧。此时造一个终态 * 就是编:判死的属主始终是 durable 侧的 reaper。所以只陈述「这条行离场了」,不逼消费端结算。 * (cli 侧裁量 [ref]:渲「离场」**不清行** —— 行保留、中性离场形、active 扣除、折叠排后。) * ADDITIVE / tolerate-absent。 */ removeReason?: "orphaned"; /** [ref] §2 判死×判代合成规则:退场帧带**被退那一代**的 {@link FleetTaskRow.cycleSeq}。 * 消费端因此能把 `retire(gen=N)` 之后的 `frame(gen=N+1)` 读成**复活**(行重生,退休不粘住新代), * 而 `retire(gen=N)` 之后的 `frame(gen=N)` 读成**前代迟到帧,忽略**。缺席同 {@link FleetTaskRow.cycleSeq} * ——「无此概念」,不是「第一代」。 */ cycleSeq?: number; } | { type: "workflow"; row: FleetWorkflowRow; ts: number; } | { type: "workflow_remove"; id: string; ts: number; } | { type: "bg_notification"; notification: BgNotification; ts: number; } | { type: "hook_notice"; notice: HookNotice; ts: number; }; /** * **一次 hook 判定「未能完成」**的观测帧(cli 黑板 [ref] 点名的需求;字段名由本仓定)。 * * ── 它解决什么 ──────────────────────────────────────────────────────────────────────────────── * 用户设了 `/goal enforced`,产品对模型承诺「没达成不许停」。某一轮评估者没能给出判词 * (无内容 / 读不懂 / 被跳过)⇒ 按裁定**放行**。此刻的问题不是放行本身,是: * **用户不知道这一轮的守卫没生效**,而模型拿到的是一个与「已达成」**无法区分**的放行。 * ⇒ **产品声称了一件它这一轮没做到的事。** 方向可以 fail-open,但不能连「我这轮没看住」都不说。 * * ── 四条设计约束(逐条对应需求)────────────────────────────────────────────────────────────── * 1. 语义是「**未能评估**」,**不是**「未通过」—— 两者必须可区分,否则壳侧只能说一句更模糊的话; * 2. `reason` 机器可判(`no_content` / `unparsed` / `skipped`),与 server 侧那三行结构化日志同源; * 3. **不只服务 `/goal`**:按「hook 判定未能完成」定义,带 `event` 位;`/goal` 只是第一个消费者; * 4. 🔴 **纯 observe,不改变运行** —— 它走 fleet 流(常开推送道),**不是** `additionalContext` * (那个通道本身会让这一轮不结束,见 hook-runner 折叠处那段旁注)。 * * ⚠️ 可见性**与 `bg_notification` 同为 fail-CLOSED**(本帧**两族 kind 同规**):带 `ownerSessionId` * 的帧只投给能证明是自己的订阅方(`undefined` ⇒ 对 scoped 订阅方**丢弃**)。理由同源:一条关于别人 * 会话的通知落到这个壳上,会让用户以为是自己这轮出了问题。 * * ⚠️ `kind` 是**闭集**:第二族(`hook_non_blocking_failure`,[ref])见下。加族必须同时在 * {@link hookNoticeWire} 表态(穷举 switch ⇒ 漏表态=编译红)。 */ export interface HookDecisionUnavailable { kind: "hook_decision_unavailable"; /** 哪个 hook 事件(`Stop` / `PreToolUse` / …)。 */ event: string; /** 机器可判的原因(与 server 日志的三行同源)。 */ reason: "no_content" | "unparsed" | "skipped"; /** 补充说明(已脱敏)。例如 `skipped` 的具体原因。 */ detail?: string; } /** * **一次 hook 自己坏了**(非阻断故障)的观测帧 —— [ref],黑板 [ref](test 实测立案)/ [ref](core 定性: * 翻译层全在 server)/ [ref](CC 锚源码核实)/ [ref](server 裁量②)。 * * ── 它解决什么(与上面那族是两件事)────────────────────────────────────────────────────────── * 上面那族说的是「判定没做成」;这一族说的是「**你配的这个 hook 自己坏了**」:退出码非 0 且非 2、 * 超时、起不来、吐了坏 JSON。方向按 [ref] 裁量① **维持 CC parity 不动**(非阻断,绝不误伤任务), * 但此前故障只进服务端结构化日志 ⇒ **任务视角零可见**:用户以为自己的守卫在跑,其实每次都在坏。 * 而 registry-core `hooks.ts:536` 对这一格逐字承诺过 "any other exit = non-blocking error * (**stderr shown to the user**)" —— 承诺与实装的落差,正是本帧要补的那一格。 * * ── CC 锚(cc-decoded/pretty223.js 亲读,[ref] 已独立核实同一段)──────────────────────────── * CC 的 `hook_non_blocking_error` 附件带 `hookName` / `hookEvent` / `stderr` / `stdout` / `exitCode`, * UI 层对非 `Stop`/`SubagentStop` 事件真实渲染「`` hook error」+ stderr(优先 stderr)。 * 本帧的键就是照这份形状取的(`hookName` 的取法同 CC 的 `OG`/`AEe`:`statusMessage ?? 命令/URL/提示`)。 * * ⚠️ 与上面那族同为**纯 observe**、同走 fleet 流、同为 fail-CLOSED 可见性 —— 刻意**不开新通道** * (`kind` 这个位当年就是为「下一类观测帧」留的)。 */ export interface HookNonBlockingFailure { kind: "hook_non_blocking_failure"; /** 哪个 hook 事件(`PreToolUse` / `Stop` / …)。 */ event: string; /** * 机器可判的故障形(闭集,与 server 日志同源): * · `exit_nonzero` —— 退出码非 0。**exit 2 的归属按「这个位置有没有兑现面」判**([ref] codex R1-F2): * - 有兑现面 —— 阻断面(PreToolUse ⇒ deny / Stop / UserPromptSubmit / PreCompact ⇒ block)或 * 反馈面(PostToolUse 族 ⇒ stderr 喂模型)—— ⇒ exit 2 是**表过态的裁决/反馈**,不是故障, * **绝不进本族**; * - 没有兑现面(observe-only 的 StopFailure/PostCompact、以及 fire-and-forget 的纯 async 条目) * ⇒ 作者的 exit 2 无处兑现,**进本族**并由 `detail` 逐字说清那里没有阻断面 —— 静默丢弃正是 * [ref] 要根除的形,而不说清则会让一次有意表态被渲染成「坏 hook」; * - `asyncRewake` 的 exit 2 是**契约动作**(唤醒模型),不是故障,不进本族; * · `timeout` —— 条目超时被击杀(无退出码); * · `spawn_failed` —— 起不来 / http 连不上 / LLM 载体失败(无退出码); * · `bad_json` —— exit 0 但 stdout 看着是 JSON 却读不成决策对象(裁决静默丢失的那一形)。 */ reason: "exit_nonzero" | "timeout" | "spawn_failed" | "bad_json"; /** hook 身份(CC `OG`/`AEe` 锚:`statusMessage` 优先,否则 command / url / prompt),已脱敏+有界。 * ⚠️ http 条目取 URL 时**剥掉 query 与 fragment**([ref] codex R1-F1:端点凭据最常见的挂法是 * `?token=…`,而通用脱敏器只认 `user:pass@` 形)。要更友好的名字,作者有 `statusMessage`。 */ hookName: string; /** 条目类型 —— 没有它,`hookName` 是命令还是 URL 还是提示词全靠猜。 */ entryType: "command" | "http" | "prompt" | "agent"; /** 退出码。**超时 / spawn 失败没有退出码 ⇒ 缺席**(不铸 0 那种看着合法的假读数)。 */ exitCode?: number; /** 工具事件才有(PreToolUse/PostToolUse/PostToolUseFailure);非工具事件缺席。 */ toolName?: string; /** hook 的 stderr 摘要(已脱敏 + 截断,见 `MAX_HOOK_NOTICE_STDERR_CHARS`)。空 stderr ⇒ 缺席。 */ stderr?: string; /** 补充说明(已脱敏)。 */ detail?: string; } /** 生产者视角的观测面(hook-runner 铸的形):归属位由 boot 层在发布时补。 */ export type HookNoticeObservation = HookDecisionUnavailable | HookNonBlockingFailure; /** 租户/会话归属位 —— **只用于过滤,上 wire 前剥**(见 `hookNoticeWire`)。 */ export interface HookNoticeOwnership { /** 租户可见性(发布前带,上 wire 前剥)。 */ ownerScope?: string; /** 宿主会话(发布前带,上 wire 前剥)——scoped 订阅方的 fail-closed 判据。 */ ownerSessionId?: string; } export type HookNotice = HookNoticeObservation & HookNoticeOwnership; /** 观测帧自由文本的上限(`hookName` / `stderr` / `detail`),**含截断标记**。stderr 是**坏 hook** * 吐的东西:一条它想吐多长就多长的流,既不能整条上 wire,也不能静默切掉(hook 作者会读错自己的输出)。 */ export declare const MAX_HOOK_NOTICE_TEXT_CHARS = 512; /** * 脱敏 + 有界 —— 观测帧自由文本的唯一整形口。 * * 🔴 **先脱敏后截断**:反过来会把一枚密钥切成半截,脱敏器就再也认不出它了(fleet 行 `objective` * 那条同款旁注:SECRET-SAFE 的次序是承重的)。 * 🔴 **幂等**(输出恒 ≤ 上限,含标记):铸点(hook-runner)与发布口(本文件)各过一遍 —— 前者拿得到 * 原文所以那一遍是**承重**的,后者是**边界兜底**(挡住忘了脱敏的未来生产者)。不幂等的话第二遍会 * 把第一遍的标记再切一刀,把「原长」写成一个假数字。 */ export declare function boundedNoticeText(s: string): string; /** * 上 wire 的载荷(归属位**逐 kind 穷举剥离**,不是 rest-spread)。 * * 🔴 穷举 `switch` 而不是 `{...n}`:[ref] 闭集纪律 —— 新增一个 kind 而不在这里表态 = **编译红**, * 而 spread 会把新 kind 的任意新键(可能带内部归属/密钥)静默推上 wire。 */ export declare function hookNoticeWire(n: HookNotice): HookNoticeObservation; /** [ref]-B 缺口② (帧契约): a background child's COMPLETION notification pushed on the * ALWAYS-OPEN fleet stream — the only server-push lane an idle shell keeps subscribed, so a session-scoped bg * child finishing while the host sits idle notifies IMMEDIATELY (no more "late by one input cycle" durable-drain). * EVENT semantics, not state: never in the snapshot, no dedup key held here (the shell's task_notification * injection queue dedups by (taskId,status) — the outer belt). Row visibility stays on `task` frames * (行是状态、通知是事件,别合并). */ export interface BgNotification { /** Registry handle (`a*` domain — the TaskOutput/TaskStop key). */ taskId: string; /** The child RUN's session id (uuid domain — the a*↔uuid dual anchor), when known. */ sessionId?: string; /** Stop-cycle counter(core 1.352 terminal 帧补位;revive 周期判别键素材)——tolerate-absent。 */ seq?: number; status: "completed" | "failed" | "killed"; summary?: string; /** Background lifetime scope ([ref]), NOT the tenant scope. */ scope: "task" | "session"; parentTaskId?: string; stoppedBy?: string; resumable?: boolean; recentSteps?: Array<{ tool: string; target: string; outcome: string; }>; editedFiles?: Array<{ path: string; edits: number; }>; /** [ref]§二 LOW 补:完成通知的花费/转录锚(core BackgroundChildEvent.terminal 携带,此前被剥—— * 完成卡无花费显示、转录跳转无锚)。数值/id verbatim。 */ usage?: { totalTokens?: number; toolUses?: number; durationMs?: number; tokens?: number; turns?: number; costMicroUsd?: number; }; transcriptId?: string; /** P1-3(core 1.452,[ref] 一轮批):子代完成的**幂等锚**——同一完成的多路通知(bg_notification/ * task_notification/inbox 重放)带同一 id,消费端按它去重。verbatim;ADDITIVE/tolerate-absent。 */ completionId?: string; /** core 1.367 δ([ref]/[ref]④):委托树 root 宿主会话(固定点语义,嵌套孙代恒锚 root shell)—— * cli 跨壳重启按宿主会话归组([ref]② 台账判别 F2 残余闭环)。ADDITIVE / tolerate-absent。 */ rootSessionId?: string; /** Tenant/principal isolation key (same filter as FleetTaskRow.scope). STRIPPED before send. */ ownerScope?: string; /** The HOST session id (session-scoped children: registry owner) — the `?session=` filter key (the payload * `sessionId` above is the CHILD's uuid and would never match the caller's session). STRIPPED before send. */ ownerSessionId?: string; } /** * The process-local fleet aggregation bus. Holds the current active set + fans out deltas to SSE subscribers. * Upserts MERGE (a partial delta patches the existing row), so a publisher can emit just the field that changed * (e.g. only `tokens`) and the row stays whole. Removal drops the row (terminal). Subscribers get a snapshot of * the current set immediately, then every subsequent delta. */ export declare class FleetEventBus { private readonly now; private readonly emitter; private readonly tasks; private readonly workflows; /** [ref] / codex R1-F2:**代际墓碑** —— 已离场行的最后已知 `cycleSeq`(行本身已从 `tasks` 删掉, * 代际状态却必须活得比行久一点,否则前代迟到帧会在空 Map 上把幽灵行重铸出来)。有界 LRU。 */ private readonly retiredGen; private static readonly RETIRED_GEN_MAX; constructor(now?: () => number); /** Upsert a task row (MERGE onto any existing row by `id`) + fan out the merged row. Publishers may send a * partial `{ id, tokens }` etc.; the row keeps its other fields. */ publishTask(delta: Partial & { id: string; }): void; /** * 推一帧 hook 观测通知(事件语义,不进 snapshot —— 与 `bg_notification` 同族)。 * * 自由文本在这里再过一遍 {@link boundedNoticeText}(幂等):铸点已经过一遍(那一遍拿得到原文,是 * 承重的),本遍是**边界兜底** —— 一个忘了脱敏的未来生产者不该靠"下游会渲染得挺好看"活着。 * ⚠️ 兜底救不了「先截断后脱敏」造出的半截密钥,所以铸点那一遍**不是可选的**。 */ publishHookNotice(notice: HookNotice): void; /** Drop a task row (terminal + swept) + fan out the removal. Idempotent (a no-op if already gone). * * `opts`([ref],两键均 ADDITIVE):`removeReason:"orphaned"` = **陈述级退场**(没有结算语义,见 * {@link FleetFrame} 的 `task_remove` 臂);`cycleSeq` = 被退那一代的代际号。发布方的常规清场 * (`onTerminal`/`onChildTerminal`)不传 opts ⇒ 帧逐字节与本改动之前相同。 */ removeTask(id: string, opts?: { removeReason?: "orphaned"; cycleSeq?: number; }): void; private readonly bceClaims; private static readonly BCE_CLAIM_MAX; private static bceKey; /** BCE 车道:本车道已为该子代 uuid 建了真行 —— run-leg 车道自此让位。幂等。 */ claimBackgroundChildLane(scope: string | undefined, childUuid: string, rowId: string): void; /** BCE 车道:该子代已终态离场 —— 让位解除(后续同 uuid 的迟到 tick 回到旧行为)。幂等。 */ releaseBackgroundChildLane(scope: string | undefined, childUuid: string): void; /** run-leg 车道:该子代是否已有 BCE 真行(有 ⇒ 让位,不铸复合 id 行)。 */ backgroundChildLaneRow(scope: string | undefined, childUuid: string): string | undefined; /** [ref] 让位迁移料的读面:迁移「只补缺席位」需要看得见存活行现有什么(BCE 是权威车道,不覆盖)。 */ taskRow(id: string): Readonly | undefined; /** Upsert a workflow row (MERGE by `id`) + fan out. */ publishWorkflow(delta: Partial & { id: string; }): void; /** Drop a workflow row + fan out the removal. Idempotent. */ removeWorkflow(id: string): void; /** [ref]-B 缺口②: fan out a background-child completion notification (EVENT — not stored, never in the * snapshot; a subscriber that connects later reads the durable inbox instead). */ publishBgNotification(notification: BgNotification): void; /** The current full state (the connect-time snapshot frame). */ snapshot(): Extract; /** Subscribe to deltas. Returns an unsubscribe thunk. The caller typically sends `snapshot()` first, then every * delta this delivers — the snapshot+delta sequence is the live fleet view. * * 每个回调各自隔离([ref] 复审 R3 HIGH):扇出是同步的,修前一个抛错的订阅方会把异常传回**发布方**—— * 而发布方是 JournalingWorkflowRunStore.put/update 的投影点,core 持久化对 put 抛错 catch{} 且不推进 * storeRev ⇒ durable 行冻在 running 而 notify 已 ack,无恢复路径。投影消费方永远不该能楔死 durable * 写路;抛错记 {@link recordFailOpen}(F 类,登记闸),不上抛、不影响其余订阅方。 */ subscribe(cb: (frame: FleetFrame) => void): () => void; /** Live counts (test/observability). */ get size(): { tasks: number; workflows: number; }; private emit; } /** A run-scoped lifecycle publisher — wire it into a run's TaskEvent loop so the fleet row TRANSITIONS with the * run. `onStart()` adds the top-level row (running); `onEvent(ev)` accrues tokens (turn_end usage) + adds/updates * subagent CHILD rows (core `task_progress`, keyed by its `taskId`, nested under the run via `parentId`); * `onTerminal(status)` flips the row terminal + removes it (and its children) from the active set. No-op when no * bus is wired. The token accrual is CUMULATIVE across the run's turns (turn_end carries a per-turn delta). */ export declare function fleetRunPublisher(bus: FleetEventBus | undefined, run: { runId: string; scope: string; name: string; description?: string; rootTaskId?: string; }): { onStart: () => void; onEvent: (ev: { type?: string; usage?: unknown; taskId?: string; name?: string; toolName?: string; parentToolCallId?: string; status?: string; seq?: number; }) => void; onForwardEvent: (ev: { type?: string; taskId?: string; parentTaskId?: string; name?: string; usage?: unknown; parentToolCallId?: string; status?: string; seq?: number; }) => void; /** Flip + remove one subagent CHILD row when its background completion notification arrives * (task_progress never emits a terminal tick — without this a bg child sat "running" until parent settle). * FLIP-THROUGH: publishes the terminal frame even when the row no longer exists — the parent leg's * settle removes ALL child rows (it cannot tell a settled sync child from a still-running bg child), so a bg * child that completes after parent settle otherwise had NO row to flip and the shell (auto-exit's fourth * release condition watches the viewed cid's terminal) never saw a terminal transition. Returns whether the * row EXISTED (hadRow — the observer logs it; false = the frame was synthesized flip-through). */ onChildTerminal: (taskId: string, status: string, altId?: string, toolUseId?: string) => boolean; /** [ref]①/(a) 案:`residuals` = 顶层 run 的终局残局键(有源才传,见 FleetTaskRow 上的键注)。 * 只有**真终态**(completed/failed/killed)帧带残局——parked(suspended/needs_review)的行还活着, * 没有「终局」可言,误传也不上帧。 */ onTerminal: (status: RunStatusForFleet, residuals?: { usage?: FleetTaskRow["usage"]; stoppedBy?: string; transcriptId?: string; }) => void; }; /** The run-scoped publisher handle (returned by `fleetRunPublisher`). Threaded into `runInBackground` so a * background run TRANSITIONS its fleet row exactly like the sync POST /v1/runs leg does. */ export type FleetRunPublisher = ReturnType; /** [ref]①/(a) 案:TaskResult → 终态行帧残局键映射(sync/background/resume 三条腿各自 settle,共用一处 * 映射——键名映射写三遍必漂移)。usage 与 {@link BgNotification.usage} 同形:stats.tokens→totalTokens、 * stats.toolCalls→toolUses(名对齐,消费端一个形状)。失败码 "cancelled" ⇒ stoppedBy:"user"(与 * markStopSource 的归因值同源);transcriptId 顶层 = run 的 sessionId([ref]§二拍)。缺源的键不铸。 * * 🔴 S-136(core 7.6.0 D-8):入参从**结构性可缺席** `errorCode?: string` 改成 `terminal?: TerminalCause`。 * 留着旧形不会红 —— 那正是危险所在:`TaskResult` 上已经没有 `errorCode` 这个键了,可结构性可选参数照样 * 编译通过,于是 `stoppedBy:"user"` 会**静默地永远不再铸**(取消的 run 在舰队行上从此看不出是被人杀的)。 * 终局事实一律走因由,`failureCodeOf` 是本仓读「这次失败带的是哪个码」的唯一形。 */ export declare function fleetRunResiduals(result: { sessionId?: string; terminal?: TerminalCause; stats?: { turns?: number; tokens?: number; toolCalls?: number; costMicroUsd?: number; }; } | null | undefined): { usage?: FleetTaskRow["usage"]; stoppedBy?: string; transcriptId?: string; }; /** * [ref]-B (core 1.240.0) — the PROCESS-LEVEL background-child fleet publisher, wired once * onto `RunnerDeps.onBackgroundChildEvent`. Unlike {@link fleetRunPublisher} (per-leg: dies at leg settle — * the root cause: a session-scoped bg child outliving its turn had NO forward path, fleet showed zero * child frames), this observer lives as long as the process and covers the child's WHOLE lifecycle: * spawn → row appears the moment the child registers (launch→turn-tail 零帧 fixed: no more waiting for a * first tick that a slow child never emits inside the turn); * tick → token rollup onto the row; ALSO best-effort removes the per-leg uuid-keyed TWIN row the leg's * onForwardEvent built for the same child (`${parentTaskId} ${progressTaskId}` composite — the * per-leg childId shape), so the child shows ONCE, keyed by its stable a* handle; * terminal → terminal frame under the a* row id, row leaves the fleet, and the contract * `bg_notification` frame rides the always-open stream (缺口②'s immediate idle delivery). * Rows are tagged `scope` (tenant filter) from the registry scope and `sessionId` (the `?session=` filter) * from the registry owner when session-scoped (owner IS the host sessionId; task-scoped children have no * session attribution → unscoped, back-compat visible). Names ride the same redactSecrets discipline. */ export declare function fleetBackgroundChildPublisher(bus?: FleetEventBus, log?: (msg: string, fields: Record) => void): (e: import("@sema-agent/core").BackgroundChildEvent) => void; /** BC-1 (clay-decided 2026-06-27): a top-level run's fleet-row `name` = a SHORT, single-line **objective preview** * (the readable "what is this task" label) — NOT the full objective dumped in, and NOT duplicated into `description`. * `description` is left to the publisher's LIVE-ACTIVITY update (`onEvent` tool_start → the current tool), so the row * shows `name`=objective + `description`=current activity (two DISTINCT things, killing the earlier "objective shown * twice" dup). Whitespace-collapsed + capped ≤48. (A subagent CHILD row's name = its agent-type — BC-2.) * SECRET-SAFE: redact secret shapes (redactSecrets) BEFORE truncating — a secret in the objective must not leak in * the row name. The durable objective is already redacted; the name preview was asymmetric (raw) until this. */ export declare function fleetRunLabels(objective: string | undefined): { name: string; }; /** * `runStatusToFleet` 的入参**闭集**([ref] P1-④)。两段: * · `RunRecord["status"]` = `"running" | core.TaskStatus` —— run **行**状态词表,属主 * `plugins/store-contracts.ts`(core 加成员 ⇒ 下面那条 switch 的 `never` 臂收不敛 ⇒ tsc 红); * · 四个**非行状态**的结算词:`queued`(未落行的排队态)、`error`(历史别名)、`killed`/`cancelled` * (取消腿自己传,不是行上会出现的词)。 * * 🔴 为什么必须是闭集而不是 `string`:开集签名让「词表加成员 ⇒ 编译红」在结构上不可能,而这条映射的 * miss 臂代价是**不可见的**(行折成 idle、removal 集不收 ⇒ fleet 永久滞留 stale 行)。2026-06-28 * on-box e2e 逮到的 `blocked` 泄漏就是这么来的;单员回归钉只钉住昨天的成员。 */ export type RunStatusForFleet = RunRecord["status"] | "queued" | "killed" | "cancelled" | "error"; /** * fleet 行的**离场**判据(`onTerminal` 的 removal 集)—— `FleetTaskStatus` 的穷举 switch。 * * 与 {@link runStatusToFleet} 同族([ref] P1-④ 证据段点名):此前是消费点手抄的 * `=== "completed" || === "failed" || === "killed"` 三词字面比对,fleet 词表加成员时同样没有编译期钉。 * park 态(`waiting` / `awaiting approval`)判 false 是承重的:行还活着,必须留在 fleet 上。 */ export declare function isTerminalFleetStatus(status: FleetTaskStatus): boolean; /** Map a service run status onto the neutral fleet vocabulary the shell renders. */ export declare function runStatusToFleet(status: RunStatusForFleet, opts?: { awaitingPlanApproval?: boolean; }): FleetTaskStatus; /** * S-213③ —— **编译期同源钉**:fleet 的 `"failed"` 桶必须恰好收下 `RUN_STATUS_CLASS` 判为「非成功终局」 * 的每一个 core 词。 * * 这个词的病史:`blocked` 曾在本仓有**两个独立写者**且答案互斥 —— 本文件读它为「非成功终局(agent * 放弃)⇒ 行离场」,而 `observability/run-terminal.ts` 读它为「治理判决,不是失败 ⇒ 不打行」。两处 * 各自穷举、各自正确地编译,结果是一条 `report_blocked` 的 run 在面板上离场、在引擎日志里一个字都没有。 * 语义收归 `plugins/store-contracts.ts` 的那张表之后,本行不再自持判断:表把某个词挪出/挪进 * `unsuccessful`(或 core 新增一个非成功终态)而上面的 switch 没跟上 ⇒ 下面这条收不敛到 `never` ⇒ tsc 红。 * (`"error"` 是 server 自有别名、不在那张表的辖域内,故不在本钉的两边出现。) */ type FleetUnsuccessfulBucket = "failed" | "blocked"; type _GuardFleetUnsuccessfulBucket = AssertAllKeysHandled>; /** 反向门(与 `core-keyset-guard.ts` 的 `_Guard*Reverse` 同形):桶里留了一个表**不再**判为非成功终局的 * 词同样红 —— 只有正向门时,把 `blocked` 挪回 `succeeded` 而 switch 照旧塞进 failed 桶,并集覆盖、 * 差集门一声不吭(本批实测:只加正向门时该变异 tsc rc=0)。两道钉一起加。 */ type _GuardFleetUnsuccessfulBucketReverse = AssertAllKeysHandled>; /** 占位使用(防 noUnusedLocals 形 lint;类型别名本身零运行时)。 */ export type FleetVocabGuards = [_GuardFleetUnsuccessfulBucket, _GuardFleetUnsuccessfulBucketReverse]; export {}; //# sourceMappingURL=fleet-bus.d.ts.map