/** * [ref] A10:composition root 分段 —— 后台 reaper / 维护 tick。 * * 纯搬运:函数体逐字来自 `main.ts`(原 2387-2599 行),缩进不变;新增的只有 import 与 * `startReapers(ctx)` 包壳。 * * ⚠️ **位置即契约**: * 1. 本段必须在 `createHttpServer` **之前**调用 —— D-D SLA-timer 的 deny-sweep 是 server 造出来的, * 故经 `getRunDenySweep()` 晚绑(`let runDenySweep` 仍留在 main.ts,赋值点仍在 server 之后)。 * 这是本次搬运里**唯一**一行改写(`runDenySweep?.(…)` → `getRunDenySweep()?.(…)`)。 * 2. `reaper.unref?.()` 必须紧跟 `setInterval`(定时器绝不可持住进程),故一并搬进本函数、 * 返回值只交出 handle 给收尾段 `clearInterval`。 * 3. tick 内各腿的**相对次序**逐字保留:probeClockSkew(时钟指纹)先于任何按本地时钟分桶的清算。 */ import { Runner, type RunnerDeps, type WorkflowRunStore } from "@sema-agent/core"; import type { ServiceConfig } from "../config.js"; import { CostQuota } from "../observability/cost-quota.js"; import type { RateLimiter } from "../observability/rate-limit.js"; import type { Logger } from "../observability/logger.js"; import type { Metrics } from "../observability/metrics.js"; import type { WorkflowNotifyGate, WorkflowNotifyJournalStore } from "../orchestration/workflow-notify-journal.js"; import type { TaskAttachmentStore } from "../plugins/task-attachment-store.js"; import type { ApprovalNonceStore, CheckpointStoreFull, CostQuotaStore, ImageBake, RateLimiterStore, ServiceWorkflowJournalStore, StoreBackend, ToolResultStoreFull } from "../plugins/store-backend.js"; import type { ToolApprovalCoordinator } from "../tool-approval.js"; /** [ref] / [ref]§五(sema-comms `audits/failopen-governance-176.md` §7 "Z 类 1 条"):default consecutive-failure * count before a reaper leg's swallowed error gets a `warn` line. The ~15 `.catch(() => undefined)` sites * below are correct in DIRECTION (a hiccuping periodic sweep must never crash the whole tick loop — one bad * leg cannot be allowed to starve its ~15 siblings), but were zero-observability: an operator watching only * `warn`/`error` would never learn a leg had been silently broken for days. `5` (not `1`) is deliberate — * a transient blip (one DB reconnect, one timeout) is exactly what fail-open exists to absorb without noise; * only a STREAK crosses into "diagnosable, not transient". Exported so a future per-leg override is a * constructor-arg change, not a magic-number hunt. */ export declare const REAPER_FAILURE_WARN_THRESHOLD = 5; /** The pair of hooks {@link createThrottledReaperCatch} returns — `onSuccess` resets the streak (call on the * chain's fulfilled path), `onError` is the `.catch()`-equivalent rejection handler (throttled, never rethrows). */ export interface ThrottledReaperCatch { onSuccess: () => void; onError: (err: unknown) => void; } /** * Builds a NAMED, stateful throttle for one reaper leg: attach as `.then(() => guard.onSuccess(), guard.onError)` * in place of a bare `.catch(() => undefined)` (the two-arg `.then` form still catches a rejection from any * earlier link in the chain — same coverage as the `.catch()` it replaces — while also observing the FULFILLED * path, which a plain `.catch()` cannot). Behavior this closure owns, unchanged reaper scheduling/cleanup: * - a successful run resets the consecutive-failure streak to 0 (one hiccup mid-outage doesn't inflate the count); * - a failure increments the streak; only once the streak reaches `threshold` (default * {@link REAPER_FAILURE_WARN_THRESHOLD}) does ONE `warn` line fire (reaper name + streak length + the most * recent error's message) — then the streak resets to 0 so a STILL-broken leg reports again after another * full `threshold` run, rather than going silent forever after its first report. * One instance must be created ONCE per named leg (outside the tick's `setInterval` callback) so the streak * survives across ticks — a fresh instance per tick would never accumulate past 1. */ export declare function createThrottledReaperCatch(name: string, logger: Logger, threshold?: number): ThrottledReaperCatch; /** [ref] —— 维护 tick 真正消费的那**一手**(窄口;理由见 `ReapersCtx.permissionRuleStores`)。 * 可选成员:File 车道没有这一面(追加日志的压实是另一件事,如实登记在 `rules-consent.ts` 的 * `PermissionRuleStoreBundle.reapExpired` 注里),缺席 ⇒ 本腿零调用。 */ export interface PermissionRuleRetentionSweeper { reapExpired?(nowMs: number): Promise; } export interface ReapersCtx { config: ServiceConfig; logger: Logger; metrics: Metrics; localRoot: string; backend: StoreBackend | undefined; subRunner: Runner; runStore: ReturnType | undefined; checkpointStore: CheckpointStoreFull | undefined; rateLimiter: RateLimiter | RateLimiterStore | undefined; costQuota: CostQuota | CostQuotaStore | undefined; toolResultStore: ToolResultStoreFull | undefined; fileHistoryStore: ReturnType | undefined; taskAttachmentStore: TaskAttachmentStore | undefined; imageBakes: ImageBake | undefined; worktreeReap: (() => Promise) | undefined; workflowNotifyGate: WorkflowNotifyGate | undefined; /** [ref]/[ref] recover 的两半 opts(boot 与周期扫描共用,单点合成在 workflow-orchestration)。 */ workflowRecoverOpts: Parameters[0]; workflowJournalStore: ServiceWorkflowJournalStore | undefined; sqlWorkflowRunStore: WorkflowRunStore | undefined; workflowNotifyJournal: WorkflowNotifyJournalStore | undefined; rosterStore: RunnerDeps["rosterStore"]; backgroundAgentStore: RunnerDeps["backgroundAgentStore"]; mailboxStore: RunnerDeps["mailboxStore"]; /** [ref] 车6:流内审批撤卡帧的投递口(收敛器/孤儿代打腿产出的帧经它反查 broker 的活连接;[ref] 起 * 活连接里的 durable 腿 ctx 会把帧落进本腿账本,sync 腿仍 live)。 * `TOOL_APPROVAL_ENABLED=false` 的部署恒 undefined ⇒ 收敛器照常收敛,只是不发通知帧(壳侧靠 * 重连 preamble 对账,见 approval-card.ts 的 `ApprovalRevokeFrame` 顶注)。 */ toolApproval: ToolApprovalCoordinator | undefined; /** [ref]:规则店的**保留期口**。`PERMISSION_RULES_ENABLED=false` 或后端没实装 ⇒ undefined * ⇒ 本腿根本不注册(零扫描),与它的同族旋钮腿同姿势。 * * 🔴 类型刻意窄到 {@link PermissionRuleRetentionSweeper} 而不是整个 `PermissionRuleStoreBundle`: * 维护 tick 对那只束的其余能力(provider / approvals / tickets)一无所知也不该知道 —— 窄口让测试 * 能用**两行**字面量驱动真实现的同一段代码,而不必为了满足一个大接口去铸 `as unknown as` 宽松断言 * (`boot/permission-rules-audit.ts` 的 `DormantRuleCounter` 是同一条判据的先例)。 * 生产传的仍是整只束(结构上满足这个窄口)。 */ permissionRuleStores: PermissionRuleRetentionSweeper | undefined; /** S-280 直连门 nonce 的消费记录店 —— 本 tick 只用它的**到期清扫**一手。行至多活 ~150 秒(= 证明还能被 * 受理的最后一刻),所以存量是「到达率 × TTL」而不是历史总量,清扫无需分批。无 backend 的 env-only * 部署 ⇒ undefined ⇒ 本腿零调用(那种部署也没有 durable 审批门)。 */ approvalNonceStore: ApprovalNonceStore | undefined; /** 晚绑(server 造出来才有)——见文件头「位置即契约」①。 */ getRunDenySweep: () => ((now: number) => Promise) | undefined; } /** core 5.48 `reapDurableAgents` 的 `deps.sessions` 面(结构形,只列本仓要填的三格)。 */ export interface ReapSessionsFacade { unpin(sessionId: string): unknown; release(sessionId: string): Promise; listPlaced?(kind: "subagent", opts?: { olderThanMs?: number; scope?: string; }): Promise>; } /** 本仓两只会话面的窄读口(host = boot 造的真 durable 店;routed = subRunner 的路由店)。 */ interface ReapSessionsInputs { host?: { release(sessionId: string): Promise | void; listPlaced?(kind: "subagent", opts?: { olderThanMs?: number; scope?: string; }): Promise>; placementOf?(sessionId: string): Promise<{ kind: string; } | undefined>; }; routed: { unpin?(sessionId: string): unknown; release(sessionId: string): Promise | void; }; } /** * 🔴 [ref] C18(core 5.48.0 [ref],[ref] 提货单)——**分区腿武装**的判据本体。 * * core 5.48 起 `reapDurableAgents` 的 `deps.sessions` 多一只可选 `listPlaced`:在场即启用**分区收割 * 腿** —— 一条 placed 转录的属主行已经不在(put 从未落地 / 上一轮条件删赢了但 release 失败)时,行驱动 * 的那条臂**结构上看不见**它(它从行出发枚举),只有从会话侧枚举才找得到。 * * 两只面必须落在**host durable 店**上,而 release 这一格是承重的: * · `listPlaced` = host 店的分区枚举(SQL 孪生实装;local/File 后端无此面 ⇒ 键**不设**,core 那条腿 * 如实不可用 —— 绝不给一个恒返回 `[]` 的假面,那会把「枚不了」读成「没有孤儿」); * · `release` 对 **placed id** 必须走 host 店:路由店(`ForkRoutingSessionStore`)对一个重启后不认识 * 的 id 会回落 transient(进程内 no-op),那会让 core 数出一次**从未发生**的 release * (`orphanPlacedReleased` 当场变成谎)而字节还在库里。故先按**持久化元组**判(一次 PK 探针), * placed 走 host 店的真删除;其余 id 原样保留 [ref] 裁定的双店语义。 * · 🔴 探针本身失败 ⇒ **响亮抛**(codex 交叉复审 R1-[medium],验真后修:本函数的初版把探针错误 * 改写成 `undefined` 再落路由店 —— 而重启后路由店不认识 placed id ⇒ transient no-op,core 却因为 * release 正常 resolve 而 `orphanPlacedReleased++`。间歇性 DB 故障于是被**伪装成 GC 成功**:监控 * 报着「收掉了 N 条孤儿」,durable 字节一条没动。这正是本函数顶注要防的那个形,初版自己踩了。 * 抛的后果是安全的:core 两条臂的 release 都在 try 内、且计数在 release **之后**递增 ⇒ 抛 = 不计数 * + 下一 tick 重新枚举重试(失败经济学与 core 的成文一致:泄漏给下一轮,而不是谎报已清)。 * * 抽成具名工厂而不是就地闭包的理由与 `createEngineNoticeSeat` 逐字同一条:判据在**值**上 * (哪只店被调),源码文本门看不出对错,只有行为测得出来。 */ export declare function createPlacementAwareReapSessions(deps: ReapSessionsInputs): ReapSessionsFacade; /** 起后台维护 tick,返回定时器 handle(收尾段 clearInterval 用)。 */ export declare function startReapers(ctx: ReapersCtx): NodeJS.Timeout; export {}; //# sourceMappingURL=reapers.d.ts.map