/** * Backend-NEUTRAL store contracts ([ref] A3 沉底). * * The record/event/summary shapes (and the pure row-mapping helpers) shared by ALL store backends — * TiDB/MySQL, PostgreSQL, file, memory — plus their non-store consumers (http wire, trace, images, * capabilities). They used to live inside the TiDB adapters, which made every other backend (and the * HTTP layer) import a TiDB module just to name a type; this file is the neutral home. * * Rules for this file: * - ZERO dialect dependencies (no mysql2, no pg) — types + pure functions only. The only imports * allowed are core contract types and the neutral sql-row-helpers leaf. * - Adapters import their contracts from HERE; no adapter imports its twin (store-backend.ts, the * composition root, is the one place that names every concrete adapter). * - The TiDB adapters re-export these names for back-compat (the historical import surface), but new * code should import from this file. */ import type { PeerSessionTempo, TaskResult, TaskStatus } from "@sema-agent/core"; /** * **File 店族的 boot 期拒启基类**(S-384;把 `RunStoreStrictHydrateError` 的那条论证升成一条族规)。 * * 🔴 **为什么必须是一个可判别的类型,而且必须是一个共同基类**:`openStoreBackendWithFallback` 对 * **默认推导**的 local(`DB_BACKEND` 未设 —— 也就是绝大多数单机部署)有一条既有降级臂:mkdir / 只读盘 * 那类「这台机器存不了盘」的失败降级到内存继续起。而 `LocalBackend` 构造器里的每一只 File 店都可能抛出 * 一条**完全不同**的句子——「这份数据有主 / operator 要了 fail-closed / 这份账本我重放不了」——它们从那个 * catch 看长得一模一样。不给身份 ⇒ 被同一条降级臂吞掉:一次**明确的 fail-closed 表态被翻译成 fail-open**, * 整只 `StoreBackend` 被丢掉(持久 run 账本、`/v1/approvals`、park 全没),而唯一的痕迹是一行 * 「DB 不可达,已退回内存」把运维支去查连通性。 * * 此前这条纪律是**逐个 `if instanceof` 手工登记**的(`AdoptionError` / `RunStoreStrictHydrateError`), * 于是「下一只会拒启的 File 店」默认是**漏的** —— S-384 的 ask 账本拒启就这么漏了一次(替补对抗复审 * F1 [high] 实测:`DB_BACKEND` 未设时坏账本 ⇒ `{backend: undefined, degraded: true}`,不是拒启)。 * 收法:**一个基类,一条重抛**。新 File 店的拒启只要 `extends StoreBootRefusalError` 就自动落在正确的 * 一侧;忘了继承是**可见**的(它是一个必须显式选择的父类),而忘了往 if 链里加一行是不可见的。 * (core 的 `AdoptionError` 不在本仓,改不了它的继承链 ⇒ 降级臂保留它那一条 `instanceof`,共两条。) */ export declare class StoreBootRefusalError extends Error { } /** * 取消便签里的**大脑相位**快照 —— core `BrainStatus` 帧经 `brainStatusEventData` 投影后的五键子集。 * * 刻意**不含** `detail`(provider 自由文本,宽面)与 `retryInMs`/`retryInSec`(相对量,过期即失真; * 绝对量 `retryAtMs` 才是可跨进程读的那个,core 顶注已裁)。`phase` 是这只快照的存在理由:没有它 * 就没有快照(不铸半只)。 */ export interface RunBrainStatusSnapshot { phase: string; attempt?: number; maxRetries?: number; /** 等待结束的**墙钟**时刻(发端进程钟域,非单调)—— 只当近似,别与本地单调计时器比。 */ retryAtMs?: number; errClass?: string; } /** * [ref] 件3(cli [ref] sema-bug4 (d) / server [ref]):**用户取消时的现场快照**。 * * 病(取证 `audits/evidence/2026-09-03-model-failure-visibility/`):模型挂起 7 分 26 秒后用户放弃, * 账本把这条 run 记成 `failed` + `errorCode:"cancelled"` + 「stream client disconnected before * completion」—— 与「用户看了一眼不想跑了,秒按 Esc」**同形**。历史 314 条里 96 条 failed、其中 63 条 * 是 `user_disconnect`,「其中有多少条其实是模型挂死」在数据面**不可考**。 * * 本结构是那一维判别材料:取消发生时,距离**本 run 上一条引擎事件**过了多久。 * * 【语义边界(勿漂)】 * - 它**不**描述模型调用相位(首字节/重试在 core 的 stream-engine/with-retry 里,server 结构上看不见 * ——那半场是 [ref] 的 core 钩,别在这里编造)。它只说「server 这一侧多久没收到这条 run 的动静了」。 * - `lastEventKind`/`lastEventAgeMs` 的数据源是**同副本进程内**的 turn 活性登记(`turn-activity.ts`)。 * 跨副本 / 本副本重启后读不到 ⇒ 两键**诚实缺席**(缺席 = 「证不出」,不是「0ms 前刚活过」)。 * - `elapsedMs` 恒在场:run 起跑到取消的墙钟毫秒。 * - 分诊材料,不是状态机输入:任何门 / CAS / resume 判定都不许读它。 * * 落点 = 终局 `result` blob 里的一个 additive 可缺席键({@link PersistedTaskResult}),**零新列** * ——run 行的 `result` 本来就是整只 JSON 存(SQL 双方言同形),所以两个孪生都不需要 DDL 变更。 */ export interface RunCancelContext { /** 最后一条进本 run durable 账本的事件类型(`text` / `tool_start` / `status` / …)。 * 🔴 它比 {@link RunCancelContext.lastEventAgeMs} **更容易缺席**(codex 交叉复审 R1-[medium],验真后 * 改的正是这条契约):打点方里有几席拿不到事件类型(core 的 `onActivity` 第五席、resume 腿的手动 * 打点),它们只推进「时刻」不带「词」。所以两键**不是**同在同缺 —— 有 age 无 kind 是合法读数, * 意思是「本副本知道那一刻,但说不出那是条什么事件」。 */ lastEventKind?: string; /** 距最后一条 durable 事件过了多久(ms,≥0)。同副本有活性登记就在场。 */ lastEventAgeMs?: number; /** 本 run 账本尾**最后一条** `status`(BrainStatus)帧的五键快照([ref]/[ref])——「已重试几次 / * 在等什么 / 什么错类」。这条帧 server 早就在透传与落账(`trace/project.ts` 的 `brainStatusEventData`), * 取消时把最后一条摘下来当便签,**不需要 core 新钩**。sticky:后续 text/tool 事件不清它。 * 这条 run 从没出过 status 帧(或跨副本读不到)⇒ 缺席,恒不铸 null。 */ lastBrainStatus?: RunBrainStatusSnapshot; /** run 起跑到本次取消的墙钟毫秒(≥0)。常态在场;行的 `createdAt` 坏到解析不出时缺席 * ——**绝不折成 0**(0 会被读成「刚起跑就取消」,与本结构要判别的那件事正好相反)。 */ elapsedMs?: number; } /** * 落库/上 wire 的终局结果 = core 的 `TaskResult` **加上** server 自铸的可缺席侧记键。 * * 今天只有一个成员({@link RunCancelContext});additive 且可缺席,所以旧行、旧消费端逐字不受影响 * (`TaskResult` 仍可原样赋给本型)。⚠️ 这不是给「往 core 结果里塞私货」开的口子:每加一个键都要能 * 回答「为什么它属于 run 账本而不属于 core 的任务结果」——`cancelContext` 的答案是它描述的是 * **server 侧的取消现场**,core 那一侧根本不知道有人按了取消。 */ export type PersistedTaskResult = TaskResult & { cancelContext?: RunCancelContext; }; export interface RunRecord { taskId: string; sessionId: string; owner: string | null; status: "running" | TaskStatus; result: PersistedTaskResult | null; error: string | null; /** Structured failure code (1.36/1.37) denormalized from the result for SQL queryability. */ errorCode: string | null; /** Work-view correlation id (optional): groups the runs of one logical task across both client doors * (MCP façade / portal). null when the caller didn't supply one. */ jobId: string | null; /** Which SYSTEM's credential submitted the run (oa/cc-mcp/portal…), derived at the auth gate — never * self-declared. null = legacy single-token deployment (authenticated, no system identity). */ source: string | null; /** The replica currently HOLDING this run — written at createRun and TAKEN OVER by markResuming ([ref]: a * cross-replica resume moves the seat, so a reader comparing it with its own instanceId is asking "is the * live leg here NOW", not "who minted the row"). Lets a preempt/cancel/steer handler tell "this run is on * MY replica" (consult the in-process inflight/steerable map authoritatively) from "held elsewhere" (fall * back to the durable cross-replica flag). null for pre-column rows. */ instanceId: string | null; /** 🔴 S-230 P-45 —— 这条 run 上**挂没挂着取消请求**(耐久 `cancel_requested` 列的投影,跨副本)。 * * 语义就是那一列的字节:`POST /v1/runs/:id/cancel` 的 running 臂 CAS 成功即置位,持有副本的心跳拍 * poll 到它就 abort;`markResuming`(续跑认领)把它清零 —— 一次撞上 suspend 的取消不许在恢复之后 * 再杀一遍(那条 reset 的理由逐字在 run-store-sql `markResuming` 上)。 * * 为什么是**行上的一位**而不是读面再查一次:列早就在(三个后端同名),只是行投影没带它 —— 正是 * 本仓 L1 键集门盯的那族「投影剥键」。读面每次 poll 再发一条 `isCancelRequested` 查询,是给热路径 * 加一次往返去拿一个本来就在同一行里的字节。 * * ⚠️ **终局行上这一位读作「历史」不是「结论」**:取消的终局事实是 `status`+`errorCode:"cancelled"` * (那条腿不经这一位),而 suspended 臂的取消**根本不置位**(它直接终局化行)。消费端判「这条 run * 被取消了没有」看终局码;判「一条还在跑的 run 上有没有人按过取消」才看这一位。 */ cancelRequested: boolean; /** First ~120 chars of the objective (secret-redacted at write) — the list's "what is this task" glance. * null for rows predating the column. */ objectivePreview: string | null; createdAt: string; updatedAt: string; } /** * `RunRecord["status"]`(= `"running" | TaskStatus`)的**唯一分类表** —— 词表的属主 = 判据的属主, * 与类型定义同住一个文件。四个类: * · `live` —— 还在跑; * · `succeeded` —— 成功终局; * · `unsuccessful` —— **非成功**终局。两个词:`failed`(限额/供应商/中止/无效输出)与 `blocked` * —— 后者是 core `TerminalCause` 的 `{kind:"blocked", reason}`,d.ts 逐字为「the agent could not * finish and said why」= **agent 自报的终态**(说完就结束,不等人、不留 checkpoint)。它**不是** * 治理判决:门拒绝那条腿在 core 里走 `paused`/`failed`,永远不铸这个词; * · `parked` —— 停在门上还能再动(`suspended` 审批/资源门、`needs_review` 评审门)。 * * 🔴 为什么是**一张表**而不是三张 switch(S-213③,2026-09-11):`blocked` 这个词此前在本仓有**两个 * 独立写者**且答案互斥 —— `observability/run-terminal.ts` 判它是「治理判决,不是失败 ⇒ 不打行」, * `fleet/fleet-bus.ts` 判它是「非成功终局(agent 放弃)⇒ failed 桶、行离场」。两处各自穷举、各自正确 * 地编译,而一条 `report_blocked` 的 run 在面板上离场、在引擎日志里一个字都没有。三张手抄的 switch * 就是三个写者;收成一张表之后,词的语义只有这一处能改。 * * 🔴 为什么这**不是**当年被否掉的 `ReadonlySet`([ref] 二轮扫描 finding):那份集合的门对象是 * `string`,对词表两个方向都无钉 —— 增词静默判「非终局」、删词留一个死字面量没人说话。本表两个方向 * 都是**编译期**问题:`satisfies Readonly>` 要求每个词都有 * 一行(core 加成员 ⇒ 缺行 ⇒ tsc 红,加成员的人被迫当场回答「这个新态算哪一类」),而多出一行 * core 已经没有的词同样红(超出键)。终局词表真变过(core 5.8.0 把 `"timeout"` 从 `TaskStatus` 删了)。 * * 判据不许再被手抄成第二份:消费点(`approval-reconciler.ts` 判据 2、`http/routes/runs.ts` 的取消后 * 复读、`observability/run-terminal.ts` 的级别判、`fleet/fleet-bus.ts` 的 failed 桶编译钉)都从这里取。 */ export type RunStatusClass = "live" | "succeeded" | "unsuccessful" | "parked"; export declare const RUN_STATUS_CLASS: { readonly running: "live"; readonly completed: "succeeded"; readonly failed: "unsuccessful"; readonly blocked: "unsuccessful"; readonly suspended: "parked"; readonly needs_review: "parked"; }; /** 表判为「非成功终局」的词——**从表派生**,不是手抄的第二份清单。今天 = `"failed" | "blocked"`。 * 消费点(`fleet/fleet-bus.ts` 的 failed 桶)拿它做编译期同源钉:表翻面 ⇒ 那道钉当场红。 */ export type UnsuccessfulRunStatus = { [K in RunRecord["status"]]: (typeof RUN_STATUS_CLASS)[K] extends "unsuccessful" ? K : never; }[RunRecord["status"]]; /** * 一个状态词的类;**表里没有的词 ⇒ `undefined`**。 * * 🔴 形参是 `string` 而不是 `RunRecord["status"]`(S-221):本函数的**全部工作**就是「这个词在不在表里」, * 把入参收成闭集等于在签名上宣称「传进来的一定是表内词」,而下一行的 `undefined` 臂正是为了不是的那些 —— * 签名与实现自相矛盾时,消费点只能靠 `as RunRecord["status"]` 硬转把窄读口(`{ status: string }`, * 三形 store 共同面)喂进来,而那正是本文件头注点名的 `rowToRun` 裸 cast 的同一条病。闭集词照样能传 * (联合 ⊂ string),编译期的执法点在上面那张表的双向钉,不在这里。 * * 导出面(S-221):`plugins/e2b-orphan-reclaim.ts` 的三臂判决(还能再动 / 终局 / 读不懂)需要的正是 * 「类 + unknown」这一对,而不是三条 boolean 谓词各问一遍 —— 它此前因此自带了第二份词表。 * * 🔴 查的是 **null 原型**的那只表,不是 `RUN_STATUS_CLASS` 本身(codex 对抗复审 r1 [high],亲验后修): * 普通对象字面量的查表**穿原型链** —— `status` 落在 `constructor` / `toString` / `__proto__` / * `hasOwnProperty` / `valueOf` 上时查到的是 `Object.prototype` 的成员而不是 `undefined`,于是 * 「读不懂的词」这一臂被整条绕过。亲验读数(修前):`runStatusClassOf("constructor")` 返回 * `function Object`。这类值是**真可达**的(`rowToRun` 把无约束的 SQL 文本列裸 cast 成本联合),而消费点 * 据此做的是不可逆动作。修在此处一次,而不是在每个消费点各写一句 `Object.hasOwn` —— 后者要靠每个消费点 * 各记得一次,属主只有一个。 * * 🔴 运行期未知词是**真可达**的([ref] codex R1 真缺陷,红先复现过):`plugins/run-store-sql.ts` 的 * `rowToRun` 把无约束的 SQL 文本列**裸 cast** 成本联合,滚动升级里一个更新的副本写入的新状态词会被 * 旧副本读到。三张派生判据对 `undefined` 一律答 `false`(保守方向,逐条理由见各自头注)——不是静默 * 兜底,而是「本进程读不懂这个词,少动一次远好过错杀」的显式表态;真正的执法点是上面那张表的编译期 * 双向钉,新词进词表的**那一刻**就红。 */ export declare function runStatusClassOf(status: string): RunStatusClass | undefined; /** * 「这一行已经走到**终局**了吗」——终局 = 成功 or 非成功(completed / failed / blocked); * 非终局 = running / suspended / needs_review(后两者都还能再动)。 * * 未知词判**非终局**:判成「终局」的代价不可逆 —— `approval-reconciler` 判据 2 会把一张还活着的 ask * 永久打成 DENIED/routing_failure,取消路由也会把这一行当终局上报。 */ export declare function isTerminalRunStatus(status: RunRecord["status"]): boolean; /** * 「这一行的终局是**非成功**的吗」(S-213③ 的共享判据)——`failed` 与 `blocked` 两词。 * * 🔴 它回答的问题与 {@link isTerminalRunStatus} **不同**:后者只问「还会不会再动」,本判据问「这次任务 * 到底成没成」。凡是「这条 run 该不该在失败面上出声/离场」的判断都读这一条,不许再手抄 * `=== "failed" || === "blocked"`(那正是 `blocked` 长出两个互斥读法的成因)。 * * 未知词判**不是**非成功:对读不懂的词保持沉默,好过按 error 级刷屏。 */ export declare function isUnsuccessfulRunStatus(status: RunRecord["status"]): boolean; /** * 「这一行**停在门上**(park)了吗」(扫描P2,2026-08-12)。 * * 🔴 为什么必须有名字:park 是**两个词**,不是一个 —— `suspended`(tool_approval / policy_ask / human / * resource_limit 门)与 `needs_review`(`plan_review` / dry-run 拦截门,写点 = 各 store 的 * `setNeedsReview`)。两者的行为面**完全一致**:行还活着、`task_active` claim 还占着、checkpoint 还能被 * 决议。凡是「行是不是还停着」的判断只手抄 `=== "suspended"` 的地方,对 plan_review 腿一律给出**相反** * 结论 —— 它会被当成终局:取消谎报 "already terminal — no-op"(claim 从此无人释放,正是 [ref] 那个把会话 * 锁死的指纹)、trace 面无帧、leader 面不触发 park 反应。这条判据的存在就是为了不再有第三次手抄。 * * 未知词的方向与 {@link isTerminalRunStatus} 同向保守:判**不是** park。理由是消费点会据此做**不可逆** * 动作(cancel 的恢复把手会 CAS-expire 一张 checkpoint);对一个本进程读不懂的状态,少动一次远好过错杀。 */ export declare function isParkedRunStatus(status: RunRecord["status"]): boolean; /** * 会话的**活动词**(core `PeerSessionTempo`)——「这条会话现在在忙还是闲」的**唯一**一张表([ref])。 * * 🔴 为什么键是 {@link RunStatusClass} 而不是状态词本身:那张状态词表(`RUN_STATUS_CLASS`)已经是这条 * 车道上唯一的词表属主,它的双向编译钉(core 加词 / 删词当场红)是本表白拿的。按状态词再写一张表 * 等于给同一族词开第二个写者 —— 正是那张表顶注里 `blocked` 长出两个互斥读法的成因。 * * 🔴 两处读、一处定义:core 的 peer 目录席(`src/peer-directory.ts` 的 `PeerSessionRecord.tempo`)与 * `GET /v1/sessions` 列表行的 `tempo` 列读的是**同一次调用**的结果 —— 同一条会话在两个面上不可能给出 * 两个不同的忙闲。 * * 🔴 **未知词 / 无读数 ⇒ 词缺席(`undefined`),不是 `idle`**(诚实缺席,不是静默兜底): * · `lastStatus === ""` 有两个真来源 ——(a)SQL 双生:这条会话**还没有任何 run**;(b)local 文件车道: * 该 lister **结构上说不出** run 状态(`local-session-store.ts:465` 自陈的诚实降级 `lastStatus: ""`)。 * 两者都不是「我知道它闲着」,所以都不许铸出 `idle` 这个断言; * · 读不懂的词(滚动升级里新副本写入的新状态词经裸 cast 读到本进程)同判 —— 与 * {@link isTerminalRunStatus} 族的保守方向一致。 * · **缺席怎么渲染是 core 的辖域,本仓一个字都不复述**(S-514-JU:此处原写着「core 对缺席渲染成 `idle`」 * 并抄了当时 `list-agents-tool.js` 的旧三元 —— core 7.24.0 [ref] 把那一臂改成了一张 own-key 表 + * `unknown` 兜底,于是那句复述当天就成了假话,而它在本仓被抄了六处)。本表只负责**铸不铸键**; * 渲染词由**跑真 core 的黑盒**钉住(`test/peer-lane-e2e.test.ts` 的 G1 格),core 哪天再改那张表, * 红的是那一格而不是一句注释。承重的不变量只有一条,且它与渲染词无关:**缺席绝不铸 `dead`** —— * `liveness` 才是 core 会拿来**整行隐藏**的那一位(7.24.2 dist 的 `list-agents-tool.js`),而任何 tempo 词 * (含缺席)都照样列出。 */ export declare const SESSION_TEMPO_BY_RUN_CLASS: { readonly live: "active"; readonly parked: "blocked"; readonly succeeded: "idle"; readonly unsuccessful: "idle"; }; /** {@link SESSION_TEMPO_BY_RUN_CLASS} 的查表面 —— 入参是**最近一条 run 的状态词**({@link SessionSummary.lastStatus}, * 开集 `string`:它来自无约束的 SQL 文本列 / 诚实降级的空串,收成闭集等于在签名上撒谎)。 * 返回词缺席 ⇒ 调用方**不铸键**(不是铸 `idle`),理由逐字见上表头注。 */ export declare function sessionTempoOfRunStatus(lastStatus: string): PeerSessionTempo | undefined; /** * 「**证得出**这条 run 活在别的副本上」——「他副本」那一句话的**唯一判据**(S-381 收编;S-430 迁居)。 * * 成案:这个合取式此前在四处**逐字手抄**(`http/routes/runs.ts` 的 steer 臂与 interrupt 臂、同文件 * `sendHandleMiss`、`http/routes/run-memory-capture.ts`),`POST /v1/sessions/:id/mcp/reconnect` 是第五处、`fleet/fleet-reconciler.ts` 是第六处 —— * 而它的每一个合取项都是一条**被复审逐条挣出来的**纪律,不是随手的判空: * · {@link RunRecord.instanceId} 缺席(`null` / 建列之前的行 / 替身行的 `undefined`)= **不知道它在哪**, * 不是「在别处」; * · `thisInstanceId` 缺席 = 本副本连自己是谁都说不上,更不能断言别人; * · 两者都在场且**不等**才够格说「他副本」(`markResuming` 认领后该列语义 = **当前持有副本**)。 * 抄一份 = 多一个写者;收成一只谓词之后,纪律改一次就处处同改。**`status` 不进本谓词**: * `sendHandleMiss` 只对 `running` 行说这句话,而 steer 族在 park/终态两臂之后才走到这里 —— 各调用点 * 的前置臂不同族,合进来会让某一处悄悄改判。 * * 🔴 **为什么住在这里而不是 `http/route-ctx.ts`**(S-430):`src/fleet/fleet-reconciler.ts` 是第六个手抄点, * 而 `src/fleet/` 今天对 `src/http/` **零依赖**(边向恒是 http → fleet)。把消费方拉去 http 取一只纯谓词 * 会凭空造一条反向装载边;而本文件正是 {@link RunRecord} 与它的兄弟谓词({@link isTerminalRunStatus} / * {@link isParkedRunStatus})的所在 —— 「关于 run 行的判词」本来就该在行的属主这里,http 与 fleet 都已经 * 依赖它。**无别名、无再导出**:旧拼法删干净,消费方的 tsc 红就是通知。 */ export declare function runProvenElsewhere(run: { instanceId?: string | null; }, thisInstanceId: string | undefined): boolean; /** One row of the GET /v1/sessions list (CC /resume picker): a DISTINCT session (sessionId) aggregated from its * task_run rows — newest-first by last activity. The preview/status are the LATEST run's ("continue where I left * off"); the counts/timestamps span the whole session. Owner included so a fleet-wide (ops) listing is attributable. */ export interface SessionSummary { sessionId: string; owner: string | null; /** The most recent run's create time (the keyset sort key) and the session's first run's create time. */ lastActivityAt: string; firstActivityAt: string; runCount: number; /** The LATEST run's objective preview + status — what the session was last doing. */ objectivePreview: string | null; lastStatus: string; /** K-5c (shell §K): the LATEST run's task_id — the anchor the shell uses to re-attach the live event tail in ONE * hop (`GET /v1/runs/:lastRunId/events`) when resuming a picked session, instead of a second lookup. */ lastRunId: string | null; /** [ref]([ref]/[ref]):latest 终局 run 的 engine runId(`TaskResult.runId`,latest.result JSON 提取的 * 零迁移路线 —— result 终局才写 ⇒ 在跑 run 键诚实缺席);OMIT-when-absent,恒不铸 null。轴辨析与 * wire 半场见 `security.ts SessionListItem.lastTaskRunId`(同名同义)。 */ lastTaskRunId?: string; /** The auto-generated session title (cheap-model one-liner, write-once). null = not (yet) * titled OR the lister face has no session_meta (the legacy task_run aggregate / memory twins — honest degrade). */ title: string | null; } /** * S-297 —— 「这个会话**最近一条腿**是哪一条 run」的窄读面(`latestRunForSession` 的回值)。 * * 刻意**不是** {@link RunRecord}:那一形携 `result` 整只 blob,而本读面的三个消费点(run 身份 / 属主门 / * 排序口径的自证)一个都用不到它 —— 与 `listSessions` 只提标量 `lastTaskRunId` 而不拉 `result` 是同一条 * 判据([ref]:列表/指针面只拉标量,不把 blob 拖进按会话的高频读)。 * * `owner` = 该 run 的**提交 principal**(可为 null = 无属主的存量行),消费点据它判「这条腿的读面该不该 * 投给当前调用方」;它与会话的 canonical owner 会分叉,判据与先例逐字见 `run-store-sql.ts deleteBySession` * 的头注(F-A)。`createdAt` = 排序键(ISO),三孪生同一口径 `created_at DESC, task_id DESC`。 */ export interface LatestRunRef { taskId: string; owner: string | null; createdAt: string; } export interface RunEvent { seq: number; type: string; data: unknown; ts: string; } /** Coarse 4-value lifecycle the SSE reader polls for terminality (run-store parity). */ export type BakeStatus = "queued" | "running" | "done" | "failed"; /** build.sh's 8-value ordered state — UI progress bar only, NEVER stream terminality (P2.3). */ export type BakeState = "PENDING" | "BUILDING" | "PUSHING" | "VERIFYING" | "REGISTERING" | "COMPLETE" | "FAILED" | "CANCELLED"; /** Structured terminal codes the center band-composer UI keys on (P2.14): null on success / uncategorized. */ export type BakeErrorCode = "band-conflict" | "duplicate" | "disk" | "timeout" | "cancelled" | null; export interface BakeRecord { bakeId: string; profile: string; bands: string[] | null; baseRef: string | null; push: boolean; dryRun: boolean; logs: boolean; argv: string[]; status: BakeStatus; state: BakeState | null; digest: string | null; repo: string | null; ref: string | null; indexId: string | null; exitCode: number | null; error: string | null; errorCode: BakeErrorCode; manifestSha: string | null; tag: string | null; idemKey: string | null; ingestSecret: string | null; runnerId: string | null; leaseUntil: string | null; cancelRequested: boolean; requestedBy: string | null; createdAt: string; updatedAt: string; } export interface BakeEvent { seq: number; /** Runner-supplied per-line ordinal (ingest dedupe key); null for image-api-synthesized frames. */ lineOrd: number | null; kind: string; /** 'raw' for opt-in build.log tail rows (evicted first); null for structured frames. */ level: string | null; data: unknown; ts: string; } /** The validated/normalized inputs a `POST /v1/images/bakes` produces (operator free-text never reaches here). */ export interface CreateBakeInput { profile: string; bands: string[] | null; baseRef: string | null; push: boolean; dryRun: boolean; logs: boolean; /** The VETTED, normalized build.sh argv the runner runs (and ONLY this). */ argv: string[]; idemKey: string | null; requestedBy: string | null; } /** The terminal fields the auto-register / done path writes (P2.10). */ export interface BakeTerminal { status: "done" | "failed"; state: BakeState; digest?: string | null; repo?: string | null; ref?: string | null; indexId?: string | null; exitCode?: number | null; error?: string | null; errorCode?: BakeErrorCode; manifestSha?: string | null; tag?: string | null; } /** Raw SELECT row shape shared by BOTH SQL twins ([ref] S8 归位, A3 沉底 — * node-pg reads BIGINT-ish `exit_code` back as a string, hence `number | string | null`). */ export interface BakeRow { bake_id: string; profile: string; bands: unknown; base_ref: string | null; push: number; dry_run: number; logs: number; argv: unknown; status: string; state: string | null; digest: string | null; repo: string | null; ref: string | null; index_id: string | null; exit_code: number | string | null; error: string | null; error_code: string | null; manifest_sha: string | null; tag: string | null; idem_key: string | null; ingest_secret: string | null; runner_id: string | null; lease_until: Date | string | null; cancel_requested: number; requested_by: string | null; created_at: Date | string; updated_at: Date | string; } /** SELECT row → {@link BakeRecord} — shared by BOTH SQL twins ([ref] S8 归位, A3 沉底). */ export declare function mapBakeRow(r: BakeRow): BakeRecord; /** Column list mapBakeRow expects — shared by BOTH SQL twins ([ref] S8 归位, A3 沉底). */ export declare const BAKE_SELECT_COLS: string; /** Nested-build security posture (the operator console shows rootless vs privileged-dind). */ export type ImageNestedBuildMode = "none" | "docker-cli-only" | "rootless-buildkit" | "privileged-dind"; export interface ImageCapabilities { browser?: boolean; db?: boolean; /** Whether the image can build container images at all (bool — back-compat). */ nestedBuild?: boolean; /** HOW it nests (the security posture). Additive over `nestedBuild`; absent ⇒ treat as `nestedBuild`. */ nestedBuildMode?: ImageNestedBuildMode; } export interface ImagePodContract { devShmMB?: number; minMemMB?: number; minCpu?: string; readyTimeoutSec?: number; securityContext?: Record; } export type ImageStatus = "building" | "published" | "deprecated" | "failed"; export type ImageVisibility = "public" | "tenant"; export interface ImageIndexEntry { id: string; profile: string; bands: string[]; repo: string; tag: string; digest: string; toolchainVersions: Record; capabilities: ImageCapabilities; podContract: ImagePodContract; sizeBytes: number | null; status: ImageStatus; visibility: ImageVisibility; tenantId: string | null; manifestSha: string | null; recipeGitSha: string | null; generatorVersion: string | null; buildDate: string | null; supersedes: string | null; signed: boolean; createdAt: string; updatedAt: string; } /** The fields an upsert supplies (id/createdAt/updatedAt are derived/managed). id is ALWAYS the deterministic * idFor(repo,digest) — there is no explicit-id seam, so id-conflict ⟺ (repo,digest)-conflict in BOTH stores * (PG's `ON CONFLICT (id)` and MySQL's `ON DUPLICATE KEY` are provably equivalent; no false-parity surface). */ export type ImageIndexUpsert = Omit; /** Who is asking — drives visibility scoping. */ export interface ImageViewer { /** Operators see every row regardless of visibility/tenant. */ operator?: boolean; /** A tenant principal additionally sees its own `tenant` rows. */ tenantId?: string | null; } export interface ImageListFilter extends ImageViewer { profile?: string; status?: ImageStatus; /** Require a capability to be true (e.g. "browser"). */ capability?: keyof ImageCapabilities; /** Only the newest published digest per profile. */ latestOnly?: boolean; limit?: number; /** Opaque keyset cursor from a prior page's `nextCursor`. */ cursor?: string; } /** Raw SELECT row shape shared by BOTH SQL twins ([ref] S8 归位, A3 沉底 — * node-pg reads BIGINT `size_bytes` back as a string, hence `number | string | null`). */ export interface ImageRow { id: string; profile: string; bands: unknown; repo: string; tag: string; digest: string; toolchain_versions: unknown; capabilities: unknown; pod_contract: unknown; size_bytes: number | string | null; status: string; visibility: string; tenant_id: string | null; manifest_sha: string | null; recipe_git_sha: string | null; generator_version: string | null; build_date: Date | string | null; supersedes: string | null; signed: number; created_at: Date | string; updated_at: Date | string; } /** SELECT row → {@link ImageIndexEntry} — shared by BOTH SQL twins ([ref] S8 归位, A3 沉底). */ export declare function mapImageRow(r: ImageRow): ImageIndexEntry; //# sourceMappingURL=store-contracts.d.ts.map