/** * BackgroundAgentStore SQL twins([ref] S1 server 半场,[ref] 提货单②)——core * `RunnerDeps.backgroundAgentStore` / `SubagentToolOptions.background.agentStore` 的 TiDB/PG * 双方言实现:每个后台子代(`a*` 句柄)一行 durable 执行记录。 * * 语义对齐真源 = core `InMemoryBackgroundAgentStore`(dist/core/background-agent-store.js), * 契约四条([ref] 必守): * 1. `put` create-once:重复 (scope, handle) 键 → `BackgroundAgentStoreError("agent_record.already_exists")` * (唯一键原子,多副本安全);scope 缺席/空串拒收(与 core 同文案——default-deny 谓词下无 scope 的 * 行会经 fallback 面变成公共可读)。 * 2. `update` rev-CAS:整行替换,`WHERE … AND rev = expect.rev`,命中即 `rev = expect.rev + 1`; * 未命中 false(心跳静默主体在 core 调用侧,store 侧诚实——同 roster 语义条 3,绝不吞成假成功)。 * 3. `listBySession`/`listByScope` content-free:只 SELECT 摘要列(绝不拉 record_json——finalOutput/ * recentSteps 可能是大 blob);session 锚 = `sessionScoped ? owner : parentSessionId`,写入时物化为 * `session_anchor` 列(anchor 为 NULL 的行 `= ?` 恒假 = core `anchor !== sessionId` 同义)。 * 4. `delete` 条件删(可选 rev CAS);`reap` 三策略与 core 逐条同形(staleRunning 翻 failed 走 * rev-CAS 逐行,竞态输了跳过不计;terminal maxAge/keep 按 spawnedAt DESC)。[ref] LOW6 记账:裸 * `reap` 的批删 IN 无上界、stale 臂全量拉 record_json——server 自己的 retention 只走 * reapDurableAgents(core 编排,不触这两条);第三方直调超大 scope 时自行分批。 * * 行形:`record_json` = 全量真源(get 逐位读回);查询/谓词列全是投影,写入时从 record 物化 * (展示列 name/agent_type 截到列宽 + PG 侧过 lossy 清洗——真源仍在 record_json;身份键列 * scope/handle/owner/session 锚**不清洗不截断**:改写身份键会让行与 engine 查询键错位,含不可存 * 字节的身份键=写入失败 fail-loud,与 [ref] 协议数据拒绝式同向)。 * PG unstorable bytes([ref] 三层定谳):record/usage 走 `pgSafeJsonStringify`(观测/内容面 lossy * U+FFFD,与 run 行/journal 同族;终报含真 NUL 的病态输入换替换标记,内容面不失行);TiDB TEXT 存 verbatim。 * * 键列 = 字节等价(TiDB VARBINARY / PG COLLATE "C"——roster F2 同案:PAD SPACE/大小写近撞不得穿过 * scope 隔离键)。`listReapScopes()` 是 server 侧扩展(非 core 接口)——reaper 的 DISTINCT-scope 枚举, * 与 workflow-run store 的 reapAllScopes 同姿势。 */ import type { Pool } from "mysql2/promise"; import { type BackgroundAgentRecord, type BackgroundAgentRowSummary, type BackgroundAgentStore } from "@sema-agent/core"; import type { PgQueryFn } from "./pg-query.js"; import { type IndexSpec } from "./ensure-index.js"; export declare const BACKGROUND_AGENT_TABLE = "background_agent"; type ReapOpts = { maxAgeMs?: number; keep?: number; staleRunningMaxAgeMs?: number; }; type ListOpts = { status?: BackgroundAgentRecord["status"]; limit?: number; }; export declare function ensureTiDBBackgroundAgentSchema(pool: Pool): Promise; /** S-287:本 store 的索引**声明**(两方言共用一份)。PG 侧由下面的 `ensurePgBackgroundAgentSchema` 应用,MySQL 侧由 `tidb-pool.ts` 的中央 `ensureSchema` 应用(那里内联 `KEY` 已在 `CREATE TABLE` 里 ⇒ 新建库探到即零 DDL,存量库缺谁补谁)。加索引以外的 schema 变更仍归运维,见 `plugins/ensure-index.ts` 头注。 */ export declare const BACKGROUND_AGENT_INDEXES: readonly IndexSpec[]; export declare function ensurePgBackgroundAgentSchema(q: PgQueryFn): Promise; export declare class TiDBBackgroundAgentStore implements BackgroundAgentStore { private readonly pool; constructor(pool: Pool); put(record: BackgroundAgentRecord): Promise; get(handle: string, scope: string): Promise; update(handle: string, scope: string, record: BackgroundAgentRecord, expect: { rev: number; }): Promise; /** core 1.383([ref] 提货单)REQUIRED 新方法——原子守卫 CAS(claim 的 reservation/rollback/finalize * 绑定字段态非仅 rev:rev 可因无关写移动而字段义已变)。语义逐字对齐 core InMemoryBackgroundAgentStore * ({@link parkGuardConditions} 顶注)。 */ updateIf(handle: string, scope: string, record: BackgroundAgentRecord, expect: { rev: number; status?: BackgroundAgentRecord["status"]; parkedCheckpointToken?: string | null; parkClaimId?: string | null; }): Promise; listBySession(scope: string, sessionId: string, opts?: ListOpts): Promise; listByScope(scope: string, opts?: ListOpts): Promise; delete(handle: string, scope: string, expect?: { rev: number; }): Promise; /** 🔴 返回值语义 = **受影响的不同行数**,不是「翻转数 + 删除数」的和(core 1.411 起)。 * * core 1.411 把自己 InMemory 的 `return touched + doomed.size` 改成 * `return new Set([...flippedKeys, ...doomed]).size` —— 因为一行可能在**同一轮** reap 里既被翻转 * (stale running → failed)、又因为翻转后变成终态而落进 maxAge/keep 的删除集,旧写法把它算了两次。 * 这条被真双库对拆套件当场抓到(升 core 1.411 后 `tidb: reaped 计数: expected 4 to be 3`,core 1.404 * 下同文件全绿 —— A/B 判明是 core 的语义修,我们跟上)。 * * ⚠️ 两个引擎**机制不同但语义一致**:PG 有 `DELETE … RETURNING handle`,可以单语句拿到真被删的 handle * 直接并入同一个 Set;TiDB/MySQL **没有 RETURNING**,`affectedRows` 只给数量不给身份,所以按「是否已在 * 翻转集里」把 doomed 拆两批删 —— 重叠那批照样真删、只是不再计数。两边都不能用「doomed.size 当删除数」 * 近似:并发写者(真心跳复活)会让 `status <> 'running'` 守卫少删几行,那时 doomed.size 会虚高。 */ reap(scope: string, now: number, opts?: ReapOpts): Promise; /** core 1.368([ref]②)可选接口成员落位(原 server 扩展 listReapScopes 更名对齐):当前持行 * scope 枚举——reaper 的 per-scope 联合 reap 入口。JS 侧 sort 与 core InMemory 逐字节同序 * (SQL ORDER BY 按 collation,非码元序)。 */ listScopes(): Promise; } export declare class PgBackgroundAgentStore implements BackgroundAgentStore { private readonly q; constructor(q: PgQueryFn); put(record: BackgroundAgentRecord): Promise; get(handle: string, scope: string): Promise; update(handle: string, scope: string, record: BackgroundAgentRecord, expect: { rev: number; }): Promise; /** core 1.383([ref] 提货单)REQUIRED 新方法(TiDB twin 同案注释)。PG 侧占位符是显式编号 `$N` * (不像 TiDB `?` 靠数组顺序)——`n` 是跑动计数器,只在真正消耗一个占位符(非 IS NULL 分支)时前增。 */ updateIf(handle: string, scope: string, record: BackgroundAgentRecord, expect: { rev: number; status?: BackgroundAgentRecord["status"]; parkedCheckpointToken?: string | null; parkClaimId?: string | null; }): Promise; listBySession(scope: string, sessionId: string, opts?: ListOpts): Promise; listByScope(scope: string, opts?: ListOpts): Promise; delete(handle: string, scope: string, expect?: { rev: number; }): Promise; /** 返回值 = 受影响的**不同行数**(core 1.411 语义;TiDB twin 的方法头注写了完整由来 + A/B 取证)。 * PG 这半有 `DELETE … RETURNING handle`,所以真被删的 handle 直接并进同一个 Set —— 不需要 TiDB 那边的 * 「拆两批发」变通,但两边的**结果**必须逐个相等(真双库对拆套件按 core InMemory 的计数逐字比)。 */ reap(scope: string, now: number, opts?: ReapOpts): Promise; /** core 1.368 可选接口成员(TiDB twin 同案注释)。 */ listScopes(): Promise; } export {}; //# sourceMappingURL=background-agent-store-sql.d.ts.map